refactor md2Redbook skill with themes and paging
275
README.md
@@ -1,165 +1,151 @@
|
||||
# 📕 Auto-Redbook-Skills
|
||||
## 📕 Auto-Redbook-Skills(已重构版)
|
||||
|
||||
> 一个自动撰写笔记、生成图片、自动发布小红书的 Skills
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
> 自动撰写小红书笔记、生成多主题卡片、可选自动发布的 Skills
|
||||
> 当前版本对渲染脚本和样式系统做了**一次完整重构**,感谢 Cursor 的辅助开发 🙌
|
||||
|
||||
---
|
||||
|
||||
## 🆕 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 格式。**
|
||||
- **🎨 8 套主题皮肤**:默认简约灰 + Playful Geometric / Neo-Brutalism / Botanical / Professional / Retro / Terminal / Sketch
|
||||
- **📐 4 种分页模式**:
|
||||
- `separator`:按 `---` 分隔手动分页
|
||||
- `auto-fit`:固定尺寸,自动整体缩放内容,避免溢出/大面积留白
|
||||
- `auto-split`:根据渲染后高度自动拆分为多张卡片
|
||||
- `dynamic`:根据内容动态调整图片高度
|
||||
- **🧱 统一卡片结构**:外层浅灰背景(`card-container`)+ 内层主题背景(`card-inner`)+ 纯排版层(`card-content`)
|
||||
- **🧠 封面与正文一体化**:封面背景、标题渐变和正文卡片背景都按主题自动匹配
|
||||
|
||||
---
|
||||
|
||||
## ✨ 功能特性
|
||||
## 🖼 主题效果示例(来自 `demos`)
|
||||
|
||||
- 📝 **撰写笔记** - 根据既定主题,撰写小红书笔记(提示词自己调整,在 `SKILL.md`里)
|
||||
- 🎨 **生成卡片** - 根据内容自动渲染生成图片,包含 cover 和内容详情,支持 Markdown 渲染
|
||||
- 🐍 **双语言脚本** - 提供 Python 和 Node.js 两种渲染方案
|
||||
- 📤 **一键发布** - 支持直接发布到小红书(需配置 Cookie)
|
||||
> 所有示例均为 1080×1440px,小红书推荐 3:4 比例
|
||||
|
||||
### Playful Geometric(活泼几何)
|
||||
|
||||
## 🚀 快速开始
|
||||

|
||||
|
||||
### Clone 项目
|
||||
### Retro(复古怀旧)
|
||||
|
||||
Clone 项目到本地
|
||||

|
||||
|
||||
### Sketch(手绘素描)
|
||||
|
||||
> 注意:该目录在 demos 中是大写 `Sketch`
|
||||
|
||||

|
||||
|
||||
### Terminal(终端风格)
|
||||
|
||||

|
||||
|
||||
### Auto-fit 模式示例(自动缩放)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用方式总览
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
|
||||
git clone https://github.com/comeonzhj/Auto-Redbook-Skills.git
|
||||
|
||||
git clone https://github.com/comeonzhj/Auto-Redbook-Skills.git
|
||||
cd Auto-Redbook-Skills
|
||||
```
|
||||
|
||||
移动到支持 Skills 的客户端对应文件夹里:
|
||||
可以将本项目放到支持 Skills 的客户端目录,例如:
|
||||
|
||||
- For Claude : `~/.claude/skills/`
|
||||
- For Alma: `~/.config/Alma/skills/`
|
||||
- For TRAE: `/your-path/.trae/skills/`
|
||||
- Claude:`~/.claude/skills/`
|
||||
- Alma:`~/.config/Alma/skills/`
|
||||
- TRAE:`/your-path/.trae/skills/`
|
||||
|
||||
### 安装依赖
|
||||
### 2. 安装依赖
|
||||
|
||||
**Python 版本:**
|
||||
**Python:**
|
||||
|
||||
```bash
|
||||
pip install markdown pyyaml playwright python-dotenv xhs
|
||||
pip install -r requirements.txt
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
**Node.js 版本:**
|
||||
**Node.js:**
|
||||
|
||||
```bash
|
||||
cd Auto-Redbook-Skills
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
## 🎨 渲染图片
|
||||
---
|
||||
|
||||
### V2 渲染(推荐)
|
||||
## 🎨 渲染图片(Python)
|
||||
|
||||
核心脚本:`scripts/render_xhs.py`
|
||||
|
||||
```bash
|
||||
# 使用默认样式
|
||||
python scripts/render_xhs_v2.py note.md
|
||||
# 最简单用法(默认主题 + 手动分页)
|
||||
python scripts/render_xhs.py demos/content.md
|
||||
|
||||
# 指定样式主题
|
||||
python scripts/render_xhs_v2.py note.md --style sunset
|
||||
# 使用自动分页(推荐:内容长短难控)
|
||||
python scripts/render_xhs.py demos/content.md -m auto-split
|
||||
|
||||
# 指定输出目录
|
||||
python scripts/render_xhs_v2.py note.md -o ./output --style xiaohongshu
|
||||
# 使用固定尺寸自动缩放(auto-fit)
|
||||
python scripts/render_xhs.py demos/content_auto_fit.md -m auto-fit
|
||||
|
||||
# 查看所有样式
|
||||
python scripts/render_xhs_v2.py --list-styles
|
||||
# 切换主题(例如 Playful Geometric)
|
||||
python scripts/render_xhs.py demos/content.md -t playful-geometric -m auto-split
|
||||
|
||||
# 自定义尺寸和像素比
|
||||
python scripts/render_xhs.py demos/content.md -t retro -m dynamic --width 1080 --height 1440 --max-height 2160 --dpr 2
|
||||
```
|
||||
|
||||
**V2 特性:**
|
||||
- 智能分页:自动检测内容高度,自动拆分卡片
|
||||
- 固定尺寸:所有卡片固定 1080×1440px
|
||||
- 多种样式:7种预设主题风格
|
||||
**主要参数:**
|
||||
|
||||
### V1 渲染(旧版)
|
||||
| 参数 | 简写 | 说明 |
|
||||
|------|------|------|
|
||||
| `--theme` | `-t` | 主题:`default`、`playful-geometric`、`neo-brutalism`、`botanical`、`professional`、`retro`、`terminal`、`sketch` |
|
||||
| `--mode` | `-m` | 分页模式:`separator` / `auto-fit` / `auto-split` / `dynamic` |
|
||||
| `--width` | `-w` | 图片宽度(默认 1080) |
|
||||
| `--height` | | 图片高度(默认 1440,`dynamic` 为最小高度) |
|
||||
| `--max-height` | | `dynamic` 模式最大高度(默认 2160) |
|
||||
| `--dpr` | | 设备像素比,控制清晰度(默认 2) |
|
||||
|
||||
> 生成结果会包含:封面 `cover.png` + 正文卡片 `card_1.png`、`card_2.png`...
|
||||
|
||||
---
|
||||
|
||||
## 🎨 渲染图片(Node.js)
|
||||
|
||||
脚本:`scripts/render_xhs.js`,参数与 Python 基本一致:
|
||||
|
||||
```bash
|
||||
# Python 版本
|
||||
python scripts/render_xhs.py note.md
|
||||
# 默认主题 + 手动分页
|
||||
node scripts/render_xhs.js demos/content.md
|
||||
|
||||
# Node.js 版本
|
||||
node scripts/render_xhs.js note.md
|
||||
# 指定主题 + 自动分页
|
||||
node scripts/render_xhs.js demos/content.md -t terminal -m auto-split
|
||||
```
|
||||
|
||||
**注意:** V1 版本当内容过长时可能出现溢出,建议手动使用 `---` 分隔内容。
|
||||
---
|
||||
|
||||
## 📤 发布到小红书
|
||||
|
||||
### 1. 配置 Cookie
|
||||
|
||||
复制 `env.example.txt` 为 `.env`,填入小红书 Cookie:
|
||||
|
||||
```bash
|
||||
cp env.example.txt .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件:
|
||||
编辑 `.env`:
|
||||
|
||||
```
|
||||
```env
|
||||
XHS_COOKIE=your_cookie_string_here
|
||||
```
|
||||
|
||||
**获取 Cookie 方法:**
|
||||
> 获取方式:浏览器登录小红书 → F12 → Network → 任意请求的 Cookie 头,复制整串。
|
||||
|
||||
1. 在浏览器中登录 [小红书](https://www.xiaohongshu.com)
|
||||
2. 打开开发者工具(F12)
|
||||
3. 在 Network 标签中查看任意请求的 Cookie 头
|
||||
4. 复制完整的 cookie 字符串
|
||||
|
||||
### 2. 发布笔记
|
||||
|
||||
Skills 会自动发布,也可以手动执行:
|
||||
### 2. 手动发布(可选)
|
||||
|
||||
```bash
|
||||
python scripts/publish_xhs.py \
|
||||
@@ -174,78 +160,67 @@ python scripts/publish_xhs.py \
|
||||
|------|------|
|
||||
| `--private` | 设为私密笔记 |
|
||||
| `--post-time "2024-01-01 12:00:00"` | 定时发布 |
|
||||
| `--api-mode` | 通过 xhs-api 服务发布 |
|
||||
| `--dry-run` | 仅验证,不实际发布 |
|
||||
|
||||
## 🎨 自定义样式
|
||||
---
|
||||
|
||||
### 修改背景渐变(V1)
|
||||
|
||||
编辑 `assets/card.html` 中的 `.card-container`:
|
||||
|
||||
```css
|
||||
.card-container {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
```
|
||||
|
||||
**预设渐变色:**
|
||||
|
||||
| 名称 | 渐变值 |
|
||||
|------|--------|
|
||||
| 紫蓝 | `#667eea → #764ba2` |
|
||||
| 粉红 | `#f093fb → #f5576c` |
|
||||
| 青蓝 | `#4facfe → #00f2fe` |
|
||||
| 绿色 | `#43e97b → #38f9d7` |
|
||||
| 橙黄 | `#fa709a → #fee140` |
|
||||
|
||||
### 更多样式选择(V2)
|
||||
|
||||
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)
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
md2Redbook/
|
||||
├── SKILL.md # 技能描述(AI Agent 使用)
|
||||
├── README.md # 项目文档
|
||||
├── STYLES.md # 样式选择指南
|
||||
Auto-Redbook-Skills/
|
||||
├── SKILL.md # 技能描述(Agent 使用说明)
|
||||
├── README.md # 项目文档(你现在看到的)
|
||||
├── requirements.txt # Python 依赖
|
||||
├── package.json # Node.js 依赖
|
||||
├── env.example.txt # Cookie 配置示例
|
||||
├── assets/
|
||||
│ ├── cover.html # 封面 HTML 模板
|
||||
│ ├── card.html # 正文卡片 HTML 模板
|
||||
│ ├── styles.css # 共用样式表
|
||||
│ ├── styles.css # 共用容器样式(cover-inner / card-inner 等)
|
||||
│ └── example.md # 示例 Markdown
|
||||
├── assets/themes/ # 主题样式(只控制排版 & 内层背景)
|
||||
│ ├── default.css
|
||||
│ ├── playful-geometric.css
|
||||
│ ├── neo-brutalism.css
|
||||
│ ├── botanical.css
|
||||
│ ├── professional.css
|
||||
│ ├── retro.css
|
||||
│ ├── terminal.css
|
||||
│ └── sketch.css
|
||||
├── demos/ # 各主题示例渲染结果
|
||||
│ ├── content.md
|
||||
│ ├── content_auto_fit.md
|
||||
│ ├── auto-fit/
|
||||
│ ├── playful-geometric/
|
||||
│ ├── retro/
|
||||
│ ├── Sketch/
|
||||
│ └── terminal/
|
||||
└── scripts/
|
||||
├── render_xhs_v2.py # Python 渲染脚本 V2(推荐)
|
||||
├── render_xhs_v2.js # Node.js 渲染脚本 V2(推荐)
|
||||
├── render_xhs.py # Python 渲染脚本 V1
|
||||
├── render_xhs.js # Node.js 渲染脚本 V1
|
||||
├── render_xhs.py # Python 渲染脚本(支持主题 + 分页模式)
|
||||
├── render_xhs.js # Node.js 渲染脚本
|
||||
└── publish_xhs.py # 小红书发布脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **Cookie 安全** - Cookie 包含登录凭证,请勿泄露或提交到版本控制
|
||||
2. **Cookie 有效期** - 小红书 Cookie 会过期,需定期更新
|
||||
3. **发布频率** - 避免频繁发布,以免触发平台限制
|
||||
4. **图片尺寸** - 渲染的图片为 1080×1440px,符合小红书推荐比例
|
||||
1. **Cookie 安全**:不要把 `.env` 提交到 Git 或共享出去。
|
||||
2. **Cookie 有效期**:过期后发布失败是正常现象,重新抓一次 Cookie 即可。
|
||||
3. **发布频率**:避免短时间内高频发布,以免触发平台风控。
|
||||
4. **图片尺寸**:默认 1080×1440px,符合小红书推荐比例。
|
||||
|
||||
---
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- [Playwright](https://playwright.dev/) - 浏览器自动化渲染
|
||||
- [Marked](https://marked.js.org/) - Markdown 解析
|
||||
- [Madopic](https://github.com/xiaolinbaba/Madopic) - Markdown 渲染
|
||||
- [xhs](https://github.com/ReaJason/xhs) - 小红书 API 客户端
|
||||
- **Cursor** - 本次重构过程中提供了极大帮助 ❤️
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
|
||||
169
SKILL.md
@@ -1,18 +1,17 @@
|
||||
---
|
||||
name: Auto-Redbook
|
||||
description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片,支持多种样式主题)。
|
||||
name: xhs-note-creator
|
||||
description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片),以及发布小红书笔记。
|
||||
---
|
||||
|
||||
# 小红书笔记创作技能
|
||||
|
||||
这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成(支持7种样式主题)和智能分页渲染。
|
||||
这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成和笔记发布。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 用户需要创建小红书笔记时
|
||||
- 用户提供资料需要转化为小红书风格内容时
|
||||
- 用户需要生成精美的图片卡片用于发布时
|
||||
- 用户需要多种风格样式选择时
|
||||
|
||||
## 工作流程
|
||||
|
||||
@@ -48,11 +47,11 @@ subtitle: "副标题文案" # 封面副标题(不超过15字)
|
||||
```
|
||||
|
||||
2. 用于渲染卡片的 Markdown 文本内容:
|
||||
- 使用 `---` 分割线将正文分隔为多个卡片段落
|
||||
- 每个分段的文字控制在 200 字左右
|
||||
- 脚本会自动检测内容高度并智能分页
|
||||
- 当待渲染内容必须严格切分为独立的数张图片时,可使用 `---` 分割线主动将正文分隔为多个卡片段落(每个段落文本控制在 200 字左右),输出图片时使用参数`-m separator`
|
||||
- 当待渲染内容无需严格分割,生成正常 Markdown 文本即可,跟下方分页模式参数规则按需选择
|
||||
|
||||
完整 Markdown 文档内容示例:
|
||||
|
||||
完整示例:
|
||||
```markdown
|
||||
---
|
||||
emoji: "💡"
|
||||
@@ -60,7 +59,7 @@ title: "5个效率神器让工作效率翻倍"
|
||||
subtitle: "对着抄作业就好了,一起变高效"
|
||||
---
|
||||
|
||||
# 神器一:Notion 📝
|
||||
# 📝 神器一:Notion
|
||||
|
||||
> 全能型笔记工具,支持数据库、看板、日历等多种视图...
|
||||
|
||||
@@ -69,11 +68,11 @@ subtitle: "对着抄作业就好了,一起变高效"
|
||||
- 特色一
|
||||
- 特色二
|
||||
|
||||
---
|
||||
|
||||
# 神器二:Raycast ⚡
|
||||
# ⚡ 神器二:Raycast
|
||||
|
||||
\`\`\`
|
||||
可使用代码块来增加渲染后图片的视觉丰富度
|
||||
\`\`\`
|
||||
|
||||
## 推荐原因
|
||||
|
||||
@@ -81,82 +80,82 @@ subtitle: "对着抄作业就好了,一起变高效"
|
||||
- 原因二
|
||||
- ……
|
||||
|
||||
---
|
||||
|
||||
# 神器三:Arc 🌈
|
||||
# 🌈 神器三:Arc
|
||||
|
||||
全新理念的浏览器,侧边栏标签管理...
|
||||
|
||||
...
|
||||
|
||||
#效率工具 #生产力 #Mac软件
|
||||
```
|
||||
|
||||
### 第三步:渲染图片卡片
|
||||
|
||||
将 Markdown 文档渲染为图片卡片。**推荐使用 V2 版本脚本**,支持智能分页和多种样式。
|
||||
|
||||
#### V2 渲染脚本(推荐)
|
||||
|
||||
V2 版本新增特性:
|
||||
- ✅ **智能分页**:自动检测内容高度,超出时自动拆分到多张卡片
|
||||
- ✅ **多种样式**:支持 7 种预设样式主题
|
||||
- ✅ **字数预估**:基于字数预分配内容,减少渲染次数
|
||||
|
||||
**Python 版本:**
|
||||
将 Markdown 文档渲染为图片卡片。使用以下脚本渲染:
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
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
|
||||
python scripts/render_xhs.py <markdown_file> [options]
|
||||
```
|
||||
|
||||
**Node.js 版本:**
|
||||
- 默认输出目录为当前工作目录
|
||||
- 生成的图片包括:封面(cover.png)和正文卡片(card_1.png, card_2.png, ...)
|
||||
|
||||
#### 渲染参数(Python)
|
||||
|
||||
| 参数 | 简写 | 说明 | 默认值 |
|
||||
|---|---|---|---|
|
||||
| `--output-dir` | `-o` | 输出目录 | 当前工作目录 |
|
||||
| `--theme` | `-t` | 排版主题 | `default` |
|
||||
| `--mode` | `-m` | 分页模式 | `separator` |
|
||||
| `--width` | `-w` | 图片宽度 | `1080` |
|
||||
| `--height` | | 图片高度(`dynamic` 下为最小高度) | `1440` |
|
||||
| `--max-height` | | `dynamic` 最大高度 | `4320` |
|
||||
| `--dpr` | | 设备像素比(清晰度) | `2` |
|
||||
|
||||
#### 排版主题(`--theme`)
|
||||
|
||||
- `default`:默认简约浅灰渐变背景(`#f3f3f3 -> #f9f9f9`)
|
||||
- `playful-geometric`:活泼几何(Memphis)
|
||||
- `neo-brutalism`:新粗野主义
|
||||
- `botanical`:植物园自然
|
||||
- `professional`:专业商务
|
||||
- `retro`:复古怀旧
|
||||
- `terminal`:终端命令行
|
||||
- `sketch`:手绘素描
|
||||
|
||||
#### 分页模式(`--mode`)
|
||||
|
||||
- `separator`:按 `---` 分隔符分页(适合内容已手动控量)
|
||||
- `auto-fit`:固定尺寸下自动缩放文字,避免溢出/留白(适合封面+单张图片但尺寸固定的情况)
|
||||
- `auto-split`:按渲染后高度自动切分分页(适合切分不影响阅读的长文内容)
|
||||
- `dynamic`:根据内容动态调整图片高度(注意:图片最高 4320,字数超过 550 的不建使用此模式)
|
||||
|
||||
#### 常用示例
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
node scripts/render_xhs_v2.js <markdown_file>
|
||||
# 1) 默认主题 + 手动分隔分页
|
||||
python scripts/render_xhs.py content.md -m separator
|
||||
|
||||
# 指定输出目录和样式
|
||||
node scripts/render_xhs_v2.js <markdown_file> -o ./output --style mint
|
||||
# 2) 固定 1080x1440,自动缩放文字,尽量填满画面
|
||||
python scripts/render_xhs.py content.md -m auto-fit
|
||||
|
||||
# 查看所有可用样式
|
||||
node scripts/render_xhs_v2.js --list-styles
|
||||
# 3) 自动切分分页(推荐:内容长短不稳定)
|
||||
python scripts/render_xhs.py content.md -m auto-split
|
||||
|
||||
# 4) 动态高度(允许不同高度卡片)
|
||||
python scripts/render_xhs.py content.md -m dynamic --max-height 4320
|
||||
|
||||
# 5) 切换主题
|
||||
python scripts/render_xhs.py content.md -t playful-geometric -m auto-split
|
||||
```
|
||||
|
||||
#### 可用样式主题
|
||||
|
||||
| 样式键 | 名称 | 描述 |
|
||||
|--------|------|------|
|
||||
| `purple` | 紫韵 | 默认样式,紫蓝色渐变 |
|
||||
| `xiaohongshu` | 小红书红 | 小红书品牌色系 |
|
||||
| `mint` | 清新薄荷 | 绿色/自然调 |
|
||||
| `sunset` | 日落橙 | 粉色/日落渐变 |
|
||||
| `ocean` | 深海蓝 | 蓝绿色海洋调 |
|
||||
| `elegant` | 优雅白 | 简约灰白调 |
|
||||
| `dark` | 暗黑模式 | 深色背景,高对比度 |
|
||||
|
||||
#### 旧版渲染脚本(保留)
|
||||
|
||||
如需使用旧版(不支持自动分页):
|
||||
#### Node.js 渲染(可选)
|
||||
|
||||
```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 content.md -t default -m separator
|
||||
```
|
||||
|
||||
**旧版已知问题**:单张卡片内容过多时可能出现文字溢出,需手动用 `---` 分隔。
|
||||
Node.js 参数与 Python 基本一致:`--output-dir/-o`、`--theme/-t`、`--mode/-m`、`--width/-w`、`--height`、`--max-height`、`--dpr`。
|
||||
|
||||
### 第四步:发布小红书笔记(可选)
|
||||
|
||||
@@ -168,7 +167,7 @@ python scripts/publish_xhs.py --title "笔记标题" --desc "笔记描述" --ima
|
||||
|
||||
**前置条件**:
|
||||
|
||||
1. 在同目录下创建 `.env` 文件,配置小红书 Cookie:
|
||||
1. 需配置小红书 Cookie:
|
||||
```
|
||||
XHS_COOKIE=your_cookie_string_here
|
||||
```
|
||||
@@ -184,46 +183,30 @@ XHS_COOKIE=your_cookie_string_here
|
||||
- 尺寸比例:3:4(小红书推荐比例)
|
||||
- 基准尺寸:1080×1440px
|
||||
- 包含:Emoji 装饰、大标题、副标题
|
||||
- 样式:渐变背景 + 圆角内容区(根据所选主题变化)
|
||||
- 样式:渐变背景 + 圆角内容区
|
||||
|
||||
### 正文卡片
|
||||
- 尺寸比例:3:4
|
||||
- 基准尺寸:1080×1440px
|
||||
- 支持:标题、段落、列表、引用、代码块、图片
|
||||
- 样式:白色卡片 + 渐变背景边框(根据所选主题变化)
|
||||
- V2 版本:自动分页,单张卡片内容不会溢出
|
||||
- 样式:白色卡片 + 渐变背景边框
|
||||
|
||||
## 技能资源
|
||||
|
||||
### 脚本文件
|
||||
- `scripts/render_xhs.py` - Python V1 渲染脚本(旧版)
|
||||
- `scripts/render_xhs.js` - Node.js V1 渲染脚本(旧版)
|
||||
- `scripts/render_xhs_v2.py` - Python V2 渲染脚本(推荐 ✅)
|
||||
- `scripts/render_xhs_v2.js` - Node.js V2 渲染脚本(推荐 ✅)
|
||||
- `scripts/render_xhs.py` - Python 渲染脚本
|
||||
- `scripts/render_xhs.js` - Node.js 渲染脚本
|
||||
- `scripts/publish_xhs.py` - 小红书发布脚本
|
||||
|
||||
### 资源文件
|
||||
- `assets/cover.html` - 封面 HTML 模板(旧版)
|
||||
- `assets/card.html` - 正文卡片 HTML 模板(旧版)
|
||||
- `assets/styles.css` - 共用样式表(旧版)
|
||||
- `assets/example.md` - 示例 Markdown 文件
|
||||
- `assets/cover.html` - 封面 HTML 模板
|
||||
- `assets/card.html` - 正文卡片 HTML 模板
|
||||
- `assets/styles.css` - 共用样式表
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **V2 版本推荐**:V2 版本支持智能分页,可自动处理内容溢出问题
|
||||
2. **样式选择**:根据内容风格选择合适的样式主题
|
||||
3. **Markdown 位置**:Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录
|
||||
4. **内容长度**:建议每个 `---` 分隔的内容块控制在 200 字以内
|
||||
5. **Cookie 有效期**:发布功能的 Cookie 有过期限制,过期后需要重新获取
|
||||
6. **发布依赖**:发布功能依赖 xhs 库,需要安装:`pip install xhs`
|
||||
|
||||
## 智能分页说明
|
||||
|
||||
V2 版本的智能分页机制:
|
||||
|
||||
1. **预估阶段**:基于字数、元素类型预估内容高度
|
||||
2. **预渲染阶段**:使用 Playwright 预渲染并测量实际高度
|
||||
3. **拆分阶段**:如果内容超出,按段落/行智能拆分内容
|
||||
4. **固定输出**:每张卡片固定为 1080×1440px,确保一致性
|
||||
|
||||
这种机制确保无论内容多长,都不会出现文字溢出问题。
|
||||
1. Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录
|
||||
2. 技能目录 (`md2Redbook/`) 仅存放脚本和模板,不存放用户数据
|
||||
3. 图片尺寸会根据内容自动调整,但保持 3:4 比例
|
||||
4. Cookie 有有效期限制,过期后需要重新获取
|
||||
5. 发布功能依赖 xhs 库,需要安装:`pip install xhs`
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
.cover-container {
|
||||
width: 1080px;
|
||||
height: 1440px;
|
||||
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
|
||||
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
font-weight: 900;
|
||||
font-size: 130px;
|
||||
line-height: 1.4;
|
||||
background: linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%);
|
||||
background: linear-gradient(180deg, #111827 0%, #4B5563 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
|
||||
@@ -1,84 +1,63 @@
|
||||
---
|
||||
emoji: "🚀"
|
||||
title: "5个效率神器"
|
||||
subtitle: "让工作效率翻倍"
|
||||
emoji: "🎨"
|
||||
title: "8种超美排版风格"
|
||||
subtitle: "小红书笔记创作神器"
|
||||
---
|
||||
|
||||
# 神器一:Notion 📝
|
||||
# 活泼几何风格 💜
|
||||
|
||||
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
||||
这是 **Playful Geometric** 风格的示例,灵感来自 Memphis 设计。
|
||||
|
||||
> 一个工具替代十个 App,笔记、任务、项目管理全搞定!
|
||||
> 糖果般的色彩搭配,硬边框与圆角的对比,让内容更有活力!
|
||||
|
||||
**核心功能:**
|
||||
- 📊 灵活的数据库视图
|
||||
- 🔗 双向链接
|
||||
- 🎨 丰富的模板库
|
||||
- 👥 团队协作
|
||||
## 特点
|
||||
|
||||
- 紫色、粉色、黄色的俏皮配色
|
||||
- 硬阴影贴纸效果
|
||||
- 非对称圆角设计
|
||||
|
||||
---
|
||||
|
||||
# 神器二:Raycast ⚡
|
||||
# 新粗野主义风格 ⚡
|
||||
|
||||
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
||||
**Neo-Brutalism** 风格:RAW. LOUD. UNAPOLOGETIC.
|
||||
|
||||
> 厚重的黑色边框,高饱和度色块,这就是反设计美学!
|
||||
|
||||
## 视觉特点
|
||||
|
||||
- 5px 以上的粗边框
|
||||
- 8-10px 的硬阴影
|
||||
- 荧光黄与电光青的撞色
|
||||
|
||||
---
|
||||
|
||||
# 植物园风格 🌿
|
||||
|
||||
**Botanical** 风格带来自然清新的感觉。
|
||||
|
||||
> 森林绿与米白的搭配,仿佛置身于植物园中
|
||||
|
||||
## 设计理念
|
||||
|
||||
- 柔和的绿色系配色
|
||||
- 自然有机的圆角
|
||||
- 温暖的米色背景
|
||||
|
||||
---
|
||||
|
||||
# 更多风格选择
|
||||
|
||||
还有更多精美风格等你探索:
|
||||
|
||||
1. **Professional** - 专业商务风格
|
||||
2. **Retro** - 复古怀旧风格
|
||||
3. **Terminal** - 终端命令行风格
|
||||
4. **Sketch** - 手绘素描风格
|
||||
|
||||
使用命令:
|
||||
```bash
|
||||
# 快捷命令示例
|
||||
raycast://extensions/raycast/clipboard/clipboard-history
|
||||
python render_xhs.py example.md -t playful-geometric
|
||||
```
|
||||
|
||||
**必装插件推荐:**
|
||||
- 剪贴板历史
|
||||
- 窗口管理
|
||||
- 快捷短语
|
||||
- API 调试工具
|
||||
|
||||
---
|
||||
|
||||
# 神器三:Arc 浏览器 🌈
|
||||
|
||||
全新理念的浏览器体验:
|
||||
- 侧边栏标签管理
|
||||
- 空间分组功能
|
||||
- 内置笔记和画板
|
||||
- 极简无干扰模式
|
||||
|
||||
---
|
||||
|
||||
# 神器四:Warp 终端 🖥️
|
||||
|
||||
基于 Rust 的现代化终端:
|
||||
|
||||
```python
|
||||
# 支持 AI 智能补全
|
||||
def example():
|
||||
print("Hello Warp!")
|
||||
```
|
||||
|
||||
- ⚡ 极速性能
|
||||
- 🤖 AI 智能提示
|
||||
- 📋 自动补全
|
||||
- 🎯 分组工作区
|
||||
|
||||
---
|
||||
|
||||
# 神器五:Fig 自动补全 🔮
|
||||
|
||||
终端命令自动补全神器:
|
||||
- 数百种 CLI 工具支持
|
||||
- 可视化参数提示
|
||||
- 团队协作分享
|
||||
|
||||
---
|
||||
|
||||
# 总结 🎯
|
||||
|
||||
效率提升不在于工具多少,而在于是否**真正用起来**。
|
||||
|
||||
选择 2-3 个适合自己的工具,持续使用,形成习惯,你就能:
|
||||
|
||||
✅ 节省 50% 的时间
|
||||
✅ 减少 80% 的焦虑
|
||||
✅ 提升 100% 的专注力
|
||||
|
||||
#效率工具 #生产力 #Mac软件 #工作技巧 #神器推荐 #Notion #Raycast #Arc浏览器 #Warp #Fig
|
||||
#小红书模板 #排版设计 #内容创作
|
||||
|
||||
@@ -37,7 +37,7 @@ body {
|
||||
.cover-container {
|
||||
width: 1080px;
|
||||
height: 1440px;
|
||||
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
|
||||
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ body {
|
||||
font-weight: 900;
|
||||
font-size: 130px;
|
||||
line-height: 1.35;
|
||||
background: linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%);
|
||||
background: linear-gradient(180deg, #111827 0%, #4B5563 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
@@ -89,7 +89,7 @@ body {
|
||||
.card-container {
|
||||
width: 1080px;
|
||||
min-height: 1440px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
overflow: hidden;
|
||||
@@ -290,7 +290,7 @@ body {
|
||||
}
|
||||
|
||||
.bg-gradient-2 {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
|
||||
}
|
||||
|
||||
.bg-gradient-3 {
|
||||
|
||||
181
assets/themes/botanical.css
Normal file
@@ -0,0 +1,181 @@
|
||||
/* ============================================
|
||||
Botanical - 植物园风格
|
||||
自然柔和,清新淡雅
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #F9FAF6 (淡绿白)
|
||||
前景色: #2D3B36 (深绿灰)
|
||||
主绿色: #4A7C59 (森林绿)
|
||||
浅绿: #8FBC8F (淡绿)
|
||||
棕色: #8B7355 (木质棕)
|
||||
米色: #E8E4DC (暖米白)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #f9faf6;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #2d3b36;
|
||||
font-size: 42px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #4a7c59;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 4px solid #8fbc8f;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #3d5a48;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.4;
|
||||
padding-left: 20px;
|
||||
border-left: 6px solid #4a7c59;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
color: #4a7c59;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 粗体 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #2d3b36;
|
||||
background-color: rgba(143, 188, 143, 0.3);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 斜体 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #4a7c59;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.card-content a {
|
||||
color: #4a7c59;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid #8fbc8f;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #4a7c59;
|
||||
}
|
||||
|
||||
/* 引用块 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 30px 40px;
|
||||
background-color: #e8e4dc;
|
||||
color: #3d5a48;
|
||||
border-left: 6px solid #4a7c59;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
.card-content code {
|
||||
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 38px;
|
||||
background-color: #e8e4dc;
|
||||
color: #8b7355;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #2d3b36;
|
||||
color: #e8e4dc;
|
||||
border-radius: 12px;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 36px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, #8fbc8f, transparent);
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
margin: 35px auto;
|
||||
box-shadow: 0 4px 20px rgba(45, 59, 54, 0.1);
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px solid #e8e4dc;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #4a7c59;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border-radius: 30px;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
170
assets/themes/default.css
Normal file
@@ -0,0 +1,170 @@
|
||||
/* ============================================
|
||||
Default - 默认风格
|
||||
小红书原生风格,渐变紫色背景
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #FFFFFF (白色卡片)
|
||||
前景色: #475569 (中性灰)
|
||||
主色: #6366f1 (靛蓝紫)
|
||||
浅紫: #8b5cf6 (紫罗兰)
|
||||
灰色: #64748b (石板灰)
|
||||
边框: #e2e8f0 (浅灰边框)
|
||||
*/
|
||||
|
||||
.card-content {
|
||||
color: #475569;
|
||||
background-color: #ffffff;
|
||||
font-size: 42px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 粗体 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* 斜体 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.card-content a {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid #6366f1;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.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 #6366f1;
|
||||
padding-left: 40px;
|
||||
background: #f1f5f9;
|
||||
padding-top: 25px;
|
||||
padding-bottom: 25px;
|
||||
padding-right: 30px;
|
||||
margin: 35px 0;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
.card-content code {
|
||||
background: #f1f5f9;
|
||||
padding: 6px 16px;
|
||||
border-radius: 8px;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
font-size: 38px;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.card-content pre {
|
||||
background: #1e293b;
|
||||
color: #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: #e2e8f0;
|
||||
margin: 50px 0;
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border-radius: 30px;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
217
assets/themes/neo-brutalism.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/* ============================================
|
||||
Neo-Brutalism - 新粗野主义风格
|
||||
RAW. LOUD. UNAPOLOGETIC.
|
||||
厚重黑色边框 + 硬阴影 + 高饱和色块
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #FFFDF5 (奶油白)
|
||||
主黑色: #000000 (纯黑)
|
||||
强调红: #FF4757 (热辣红)
|
||||
强调黄: #FECA57 (荧光黄)
|
||||
强调青: #00D2D3 (电光青)
|
||||
强调紫: #A29BFE (柔和紫)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #fffdf5;
|
||||
/* 纸张纹理感 */
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 1px,
|
||||
rgba(0, 0, 0, 0.008) 1px,
|
||||
rgba(0, 0, 0, 0.008) 2px
|
||||
);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #000000;
|
||||
font-size: 42px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 900;
|
||||
padding: 0.4em 0.6em;
|
||||
background-color: #feca57;
|
||||
color: #000000;
|
||||
border: 5px solid #000000;
|
||||
box-shadow: 8px 8px 0 #000000;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 900;
|
||||
padding: 0.35em 0.6em;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
border: 4px solid #000000;
|
||||
border-left: 10px solid #ff4757;
|
||||
box-shadow: 6px 6px 0 #000000;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
padding: 0.25em 0.5em;
|
||||
color: #000000;
|
||||
background-color: #00d2d3;
|
||||
border: 4px solid #000000;
|
||||
box-shadow: 4px 4px 0 #000000;
|
||||
display: inline-block;
|
||||
margin: 40px 0 20px 0;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 加粗 - 黄色高亮块 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 900;
|
||||
color: #000000;
|
||||
background-color: #feca57;
|
||||
padding: 0.08em 0.25em;
|
||||
border: 2px solid #000000;
|
||||
}
|
||||
|
||||
/* 斜体 - 红色下划线 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #000000;
|
||||
border-bottom: 3px solid #ff4757;
|
||||
}
|
||||
|
||||
/* 链接 - 青色底纹+粗下划线 */
|
||||
.card-content a {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
background-color: #00d2d3;
|
||||
padding: 0.08em 0.2em;
|
||||
border-bottom: 4px solid #000000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #ff4757;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* 引用块 - 紫色色块+硬阴影 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 0.85em 1.1em;
|
||||
background-color: #a29bfe;
|
||||
color: #000000;
|
||||
border: 5px solid #000000;
|
||||
border-left: 12px solid #ff4757;
|
||||
box-shadow: 8px 8px 0 #000000;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* 行内代码 - 黄色背景 */
|
||||
.card-content code {
|
||||
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 38px;
|
||||
background-color: #feca57;
|
||||
color: #000000;
|
||||
padding: 0.18em 0.45em;
|
||||
border: 3px solid #000000;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 代码块 - 黑色背景+红色阴影 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #000000;
|
||||
border: 5px solid #000000;
|
||||
box-shadow: 10px 10px 0 #ff4757;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: 36px;
|
||||
line-height: 1.55;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 分割线 - 粗犷几何 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 8px;
|
||||
background-color: #000000;
|
||||
box-shadow: 5px 5px 0 #ff4757;
|
||||
}
|
||||
|
||||
/* 图片 - 厚边框框架 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 5px solid #000000;
|
||||
box-shadow: 8px 8px 0 #000000;
|
||||
margin: 35px auto;
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 5px solid #000000;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #ff4757;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 800;
|
||||
border: 3px solid #000000;
|
||||
box-shadow: 4px 4px 0 #000000;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
224
assets/themes/playful-geometric.css
Normal file
@@ -0,0 +1,224 @@
|
||||
/* ============================================
|
||||
Playful Geometric - 活泼几何风格
|
||||
Memphis 设计与现代波普艺术的融合
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #FFFDF5 (温暖奶油白 - 纸质感)
|
||||
前景色: #1E293B (石板色 800)
|
||||
主紫色: #8B5CF6 (Vivid Violet - 品牌主色)
|
||||
粉色: #F472B6 (Hot Pink - 俏皮活力)
|
||||
黄色: #FBBF24 (Amber/Yellow - 乐观积极)
|
||||
薄荷绿: #34D399 (Emerald/Mint - 清新感)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #fffdf5;
|
||||
/* 几何点阵背景装饰 */
|
||||
background-image: radial-gradient(circle, #e2e8f0 1.5px, transparent 1.5px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #1e293b;
|
||||
font-size: 42px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 800;
|
||||
padding: 0.4em 0.65em;
|
||||
background-color: #8b5cf6;
|
||||
color: #ffffff;
|
||||
border: 3px solid #1e293b;
|
||||
border-radius: 0 28px 0 28px;
|
||||
box-shadow: 6px 6px 0 #1e293b;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 800;
|
||||
padding: 0.3em 0.6em;
|
||||
background-color: #fffdf5;
|
||||
color: #7c3aed;
|
||||
border: 3px solid #1e293b;
|
||||
border-left: 10px solid #8b5cf6;
|
||||
border-radius: 0 20px 20px 0;
|
||||
box-shadow: 5px 5px 0 #f472b6;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 800;
|
||||
padding: 0.25em 0.65em;
|
||||
color: #1e293b;
|
||||
background-color: #fbbf24;
|
||||
border: 2px solid #1e293b;
|
||||
border-radius: 9999px;
|
||||
display: inline-block;
|
||||
box-shadow: 4px 4px 0 #1e293b;
|
||||
margin: 40px 0 20px 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 粗体 - 黄色高亮贴纸效果 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
background-color: #fbbf24;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 6px;
|
||||
box-shadow: 2px 2px 0 rgba(30, 41, 59, 0.2);
|
||||
}
|
||||
|
||||
/* 斜体 - 紫色强调 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #8b5cf6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 链接 - 活泼几何风格 */
|
||||
.card-content a {
|
||||
color: #8b5cf6;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
background-color: rgba(139, 92, 246, 0.1);
|
||||
padding: 0.08em 0.25em;
|
||||
border-radius: 5px;
|
||||
border-bottom: 2px solid #8b5cf6;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #8b5cf6;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 引用块 - 气泡贴纸风格 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 0.8em 1.1em 0.8em 1.6em;
|
||||
background-color: #f472b6;
|
||||
color: #1e293b;
|
||||
border: 3px solid #1e293b;
|
||||
border-left: 10px solid #fbbf24;
|
||||
border-radius: 0 28px 28px 0;
|
||||
box-shadow: 6px 6px 0 #1e293b;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* 行内代码 - 黄色贴纸 */
|
||||
.card-content code {
|
||||
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 38px;
|
||||
background-color: #fbbf24;
|
||||
color: #1e293b;
|
||||
padding: 0.15em 0.5em;
|
||||
border: 2px solid #1e293b;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 代码块 - 深色背景配紫色硬阴影 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #1e293b;
|
||||
border: 3px solid #1e293b;
|
||||
border-radius: 20px;
|
||||
box-shadow: 8px 8px 0 #8b5cf6;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: #ffffff;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-size: 36px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* 分割线 - 彩虹几何装饰条 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 10px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#8b5cf6 0px,
|
||||
#8b5cf6 24px,
|
||||
#f472b6 24px,
|
||||
#f472b6 48px,
|
||||
#fbbf24 48px,
|
||||
#fbbf24 72px,
|
||||
#34d399 72px,
|
||||
#34d399 96px
|
||||
);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
/* 图片 - 贴纸框架风格 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 4px solid #1e293b;
|
||||
border-radius: 20px;
|
||||
box-shadow: 8px 8px 0 #1e293b;
|
||||
margin: 35px auto;
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 5px solid #fbbf24;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #8b5cf6;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border-radius: 9999px;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 600;
|
||||
border: 2px solid #1e293b;
|
||||
box-shadow: 3px 3px 0 #1e293b;
|
||||
}
|
||||
176
assets/themes/professional.css
Normal file
@@ -0,0 +1,176 @@
|
||||
/* ============================================
|
||||
Professional - 专业商务风格
|
||||
简洁、稳重、可读性强
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #FFFFFF (纯白)
|
||||
前景色: #1A202C (深灰黑)
|
||||
主蓝色: #2563EB (专业蓝)
|
||||
浅蓝: #DBEAFE (淡蓝背景)
|
||||
灰色: #64748B (中性灰)
|
||||
边框: #E2E8F0 (浅灰边框)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #1a202c;
|
||||
font-size: 42px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 3px solid #2563eb;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 粗体 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
/* 斜体 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.card-content a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid #93c5fd;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* 引用块 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 30px 40px;
|
||||
background-color: #dbeafe;
|
||||
color: #1e40af;
|
||||
border-left: 5px solid #2563eb;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
.card-content code {
|
||||
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 38px;
|
||||
background-color: #f1f5f9;
|
||||
color: #0f172a;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #0f172a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 12px;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 36px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 2px;
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 35px auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border-radius: 6px;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
183
assets/themes/retro.css
Normal file
@@ -0,0 +1,183 @@
|
||||
/* ============================================
|
||||
Retro - 复古怀旧风格
|
||||
温暖的米色调和复古排版
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #FDF6E3 (复古米黄)
|
||||
前景色: #5C4033 (棕褐色)
|
||||
主色: #D35400 (复古橙)
|
||||
浅橙: #F39C12 (暖黄)
|
||||
深棕: #8B4513 (马鞍棕)
|
||||
米色: #F5DEB3 (小麦色)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #fdf6e3;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #5c4033;
|
||||
font-size: 42px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #d35400;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
border-bottom: 4px double #d35400;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #8b4513;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
color: #a0522d;
|
||||
margin: 40px 0 20px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 粗体 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
/* 斜体 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #d35400;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.card-content a {
|
||||
color: #d35400;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: wavy;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #d35400;
|
||||
}
|
||||
|
||||
/* 引用块 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 30px 40px;
|
||||
background-color: #f5deb3;
|
||||
color: #5c4033;
|
||||
border-left: 6px solid #d35400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
.card-content code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 38px;
|
||||
background-color: #f5deb3;
|
||||
color: #8b4513;
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #5c4033;
|
||||
color: #fdf6e3;
|
||||
border: 3px solid #8b4513;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 36px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 3px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#d35400,
|
||||
#d35400 10px,
|
||||
transparent 10px,
|
||||
transparent 20px
|
||||
);
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 35px auto;
|
||||
border: 4px solid #8b4513;
|
||||
box-shadow: 4px 4px 0 #d35400;
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 3px double #d35400;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #d35400;
|
||||
color: #fdf6e3;
|
||||
padding: 12px 28px;
|
||||
border-radius: 4px;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 600;
|
||||
border: 2px solid #8b4513;
|
||||
}
|
||||
198
assets/themes/sketch.css
Normal file
@@ -0,0 +1,198 @@
|
||||
/* ============================================
|
||||
Sketch - 手绘素描风格
|
||||
纸张质感、手写字体效果
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #FFFEF9 (米白纸张)
|
||||
前景色: #333333 (炭笔黑)
|
||||
主色: #555555 (铅笔灰)
|
||||
强调: #E74C3C (红色标记笔)
|
||||
蓝色: #3498DB (蓝色圆珠笔)
|
||||
黄色: #F1C40F (荧光笔)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #fffef9;
|
||||
/* 纸张网格背景 */
|
||||
background-image:
|
||||
linear-gradient(#e0e0e0 1px, transparent 1px),
|
||||
linear-gradient(90deg, #e0e0e0 1px, transparent 1px);
|
||||
background-size: 30px 30px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #333333;
|
||||
font-size: 42px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: wavy;
|
||||
text-decoration-color: #e74c3c;
|
||||
text-underline-offset: 10px;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 56px;
|
||||
font-weight: 600;
|
||||
color: #555555;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.4;
|
||||
border-bottom: 3px dashed #999999;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
color: #666666;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 粗体 - 加粗圈标 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
background-color: #f1c40f;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 斜体 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.card-content a {
|
||||
color: #3498db;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dashed;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
/* 引用块 - 便签纸风格 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 30px 40px;
|
||||
background-color: #fff9c4;
|
||||
color: #333333;
|
||||
border: none;
|
||||
box-shadow: 3px 3px 0 #ddd;
|
||||
transform: rotate(-0.5deg);
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
.card-content code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 38px;
|
||||
background-color: #f0f0f0;
|
||||
color: #555555;
|
||||
padding: 6px 16px;
|
||||
border: 2px dashed #999999;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333333;
|
||||
border: 2px solid #333333;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: 36px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 3px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#333333,
|
||||
#333333 5px,
|
||||
transparent 5px,
|
||||
transparent 10px
|
||||
);
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 35px auto;
|
||||
border: 3px solid #333333;
|
||||
box-shadow: 5px 5px 0 #ddd;
|
||||
transform: rotate(0.5deg);
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 2px dashed #999999;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
color: #333333;
|
||||
padding: 12px 28px;
|
||||
border: 2px solid #333333;
|
||||
font-size: 34px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 600;
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
194
assets/themes/terminal.css
Normal file
@@ -0,0 +1,194 @@
|
||||
/* ============================================
|
||||
Terminal - 终端/命令行风格
|
||||
黑客美学,极简科技感
|
||||
适配小红书卡片渲染
|
||||
============================================ */
|
||||
|
||||
/* 配色定义
|
||||
背景色: #0D1117 (深黑)
|
||||
前景色: #C9D1D9 (淡灰白)
|
||||
主绿: #39D353 (终端绿)
|
||||
黄色: #F0E68C (警告黄)
|
||||
青色: #58A6FF (链接蓝)
|
||||
紫色: #A371F7 (高亮紫)
|
||||
*/
|
||||
|
||||
.card-inner {
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: #c9d1d9;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', monospace;
|
||||
font-size: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.card-content h1 {
|
||||
font-size: 68px;
|
||||
font-weight: 700;
|
||||
color: #39d353;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-content h1::before {
|
||||
content: '# ';
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 54px;
|
||||
font-weight: 600;
|
||||
color: #58a6ff;
|
||||
margin: 50px 0 25px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-content h2::before {
|
||||
content: '## ';
|
||||
color: #a371f7;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 46px;
|
||||
font-weight: 600;
|
||||
color: #a371f7;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
|
||||
.card-content h3::before {
|
||||
content: '### ';
|
||||
color: #39d353;
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
.card-content p {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
/* 粗体 */
|
||||
.card-content strong,
|
||||
.card-content b {
|
||||
font-weight: 700;
|
||||
color: #39d353;
|
||||
}
|
||||
|
||||
/* 斜体 */
|
||||
.card-content em,
|
||||
.card-content i {
|
||||
font-style: italic;
|
||||
color: #f0e68c;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.card-content a {
|
||||
color: #58a6ff;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.card-content ul,
|
||||
.card-content ol {
|
||||
margin: 30px 0;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.card-content li {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-content li::marker {
|
||||
color: #39d353;
|
||||
}
|
||||
|
||||
/* 引用块 */
|
||||
.card-content blockquote {
|
||||
margin: 35px 0;
|
||||
padding: 30px 40px;
|
||||
background-color: #161b22;
|
||||
color: #8b949e;
|
||||
border-left: 4px solid #39d353;
|
||||
}
|
||||
|
||||
.card-content blockquote::before {
|
||||
content: '> ';
|
||||
color: #39d353;
|
||||
}
|
||||
|
||||
.card-content blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 行内代码 */
|
||||
.card-content code {
|
||||
font-family: inherit;
|
||||
font-size: 38px;
|
||||
background-color: #21262d;
|
||||
color: #f0e68c;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.card-content pre {
|
||||
margin: 35px 0;
|
||||
padding: 40px;
|
||||
background-color: #161b22;
|
||||
color: #c9d1d9;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 8px;
|
||||
overflow-x: visible;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-content pre code {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 36px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.card-content hr {
|
||||
margin: 50px 0;
|
||||
border: none;
|
||||
height: 2px;
|
||||
background-color: #30363d;
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.card-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 35px auto;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Tags 标签样式 */
|
||||
.tags-container {
|
||||
margin-top: 50px;
|
||||
padding-top: 30px;
|
||||
border-top: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background-color: #21262d;
|
||||
color: #39d353;
|
||||
padding: 12px 28px;
|
||||
border-radius: 30px;
|
||||
font-size: 32px;
|
||||
margin: 10px 15px 10px 0;
|
||||
font-weight: 500;
|
||||
border: 1px solid #39d353;
|
||||
}
|
||||
BIN
demos/.DS_Store
vendored
Normal file
BIN
demos/Sketch/card_1.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
demos/Sketch/card_2.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
demos/Sketch/card_3.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
demos/Sketch/card_4.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
demos/Sketch/card_5.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
demos/Sketch/cover.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
demos/auto-fit/card_1.png
Normal file
|
After Width: | Height: | Size: 637 KiB |
BIN
demos/auto-fit/cover.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
70
demos/content.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
emoji: "🚀"
|
||||
title: "5个效率神器"
|
||||
subtitle: "让工作效率翻倍"
|
||||
---
|
||||
|
||||
# 神器一:Notion 📝
|
||||
|
||||
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
||||
|
||||
> 一个工具替代十个 App,笔记、任务、项目管理全搞定!
|
||||
|
||||
**核心功能:**
|
||||
- 📊 灵活的数据库视图
|
||||
- 🔗 双向链接
|
||||
- 🎨 丰富的模板库
|
||||
- 👥 团队协作
|
||||
|
||||
---
|
||||
|
||||
# 神器二:Raycast ⚡
|
||||
|
||||
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
||||
|
||||
```bash
|
||||
# 快捷命令示例
|
||||
raycast://extensions/raycast/clipboard/clipboard-history
|
||||
```
|
||||
|
||||
**必装插件推荐:**
|
||||
- 剪贴板历史
|
||||
- 窗口管理
|
||||
- 快捷短语
|
||||
- API 调试工具
|
||||
|
||||
---
|
||||
|
||||
# 神器三:Arc 浏览器 🌈
|
||||
|
||||
全新理念的浏览器体验:
|
||||
- 侧边栏标签管理
|
||||
- 空间分组功能
|
||||
- 内置笔记和画板
|
||||
- 极简无干扰模式
|
||||
|
||||
---
|
||||
|
||||
# 神器四:Warp 终端 🖥️
|
||||
|
||||
基于 Rust 的现代化终端:
|
||||
|
||||
```python
|
||||
# 支持 AI 智能补全
|
||||
def example():
|
||||
print("Hello Warp!")
|
||||
```
|
||||
|
||||
- ⚡ 极速性能
|
||||
- 🤖 AI 智能提示
|
||||
- 📋 自动补全
|
||||
- 🎯 分组工作区
|
||||
|
||||
---
|
||||
|
||||
# 神器五:Fig 自动补全 🔮
|
||||
|
||||
终端命令自动补全神器:
|
||||
- 数百种 CLI 工具支持
|
||||
- 可视化参数提示
|
||||
- 团队协作分享
|
||||
42
demos/content_auto_fit.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
emoji: "🚀"
|
||||
title: "5个效率神器"
|
||||
subtitle: "让工作效率翻倍"
|
||||
---
|
||||
|
||||
# 神器一:Notion 📝
|
||||
|
||||
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
||||
|
||||
> 一个工具替代十个 App,笔记、任务、项目管理全搞定!
|
||||
|
||||
**核心功能:**
|
||||
- 📊 灵活的数据库视图
|
||||
- 🔗 双向链接
|
||||
- 🎨 丰富的模板库
|
||||
- 👥 团队协作
|
||||
|
||||
# 神器二:Raycast ⚡
|
||||
|
||||
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
||||
|
||||
```bash
|
||||
# 快捷命令示例
|
||||
raycast://extensions/raycast/clipboard/clipboard-history
|
||||
```
|
||||
|
||||
**必装插件推荐:**
|
||||
- 剪贴板历史
|
||||
- 窗口管理
|
||||
- 快捷短语
|
||||
- API 调试工具
|
||||
|
||||
# 总结 🎯
|
||||
|
||||
效率提升不在于工具多少,而在于是否**真正用起来**。
|
||||
|
||||
选择 2-3 个适合自己的工具,持续使用,形成习惯,你就能:
|
||||
|
||||
✅ 节省 50% 的时间
|
||||
✅ 减少 80% 的焦虑
|
||||
✅ 提升 100% 的专注力
|
||||
BIN
demos/playful-geometric/card_1.png
Normal file
|
After Width: | Height: | Size: 579 KiB |
BIN
demos/playful-geometric/card_2.png
Normal file
|
After Width: | Height: | Size: 525 KiB |
BIN
demos/playful-geometric/card_3.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
demos/playful-geometric/card_4.png
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
demos/playful-geometric/card_5.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
BIN
demos/playful-geometric/cover.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
demos/retro/card_1.png
Normal file
|
After Width: | Height: | Size: 509 KiB |
BIN
demos/retro/card_2.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
BIN
demos/retro/card_3.png
Normal file
|
After Width: | Height: | Size: 410 KiB |
BIN
demos/retro/card_4.png
Normal file
|
After Width: | Height: | Size: 447 KiB |
BIN
demos/retro/card_5.png
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
demos/retro/cover.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
demos/terminal/card_1.png
Normal file
|
After Width: | Height: | Size: 380 KiB |
BIN
demos/terminal/card_2.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
demos/terminal/card_3.png
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
demos/terminal/card_4.png
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
demos/terminal/card_5.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
demos/terminal/cover.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
24
package.json
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "md2redbook",
|
||||
"version": "1.0.0",
|
||||
"description": "小红书笔记卡片渲染工具 - Node.js 版本",
|
||||
"version": "2.0.0",
|
||||
"description": "小红书笔记素材创作工具 - 支持多种排版样式和智能分页",
|
||||
"main": "scripts/render_xhs.js",
|
||||
"scripts": {
|
||||
"render": "node scripts/render_xhs.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^11.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"playwright": "^1.40.0"
|
||||
"render": "node scripts/render_xhs.js",
|
||||
"install-browsers": "npx playwright install chromium"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaohongshu",
|
||||
"markdown",
|
||||
"image",
|
||||
"card",
|
||||
"generator"
|
||||
"image-generation",
|
||||
"social-media"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"marked": "^11.0.0",
|
||||
"yaml": "^2.3.0",
|
||||
"playwright": "^1.40.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# 小红书笔记创作技能 - Python 依赖
|
||||
# 小红书笔记创作技能依赖
|
||||
|
||||
# Markdown 解析
|
||||
# Markdown 处理
|
||||
markdown>=3.4.0
|
||||
|
||||
# YAML 解析
|
||||
PyYAML>=6.0
|
||||
|
||||
# 图片渲染 (使用 Playwright)
|
||||
# 浏览器自动化(渲染图片)
|
||||
playwright>=1.40.0
|
||||
|
||||
# 小红书 API 客户端
|
||||
xhs
|
||||
# 小红书发布
|
||||
xhs>=0.4.0
|
||||
|
||||
# 环境变量加载
|
||||
# 环境变量管理
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# HTTP 请求(API 模式)
|
||||
requests>=2.28.0
|
||||
|
||||
@@ -1,72 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书笔记发布脚本
|
||||
将生成的图片卡片发布到小红书
|
||||
小红书笔记发布脚本 - 增强版
|
||||
支持直接发布(本地签名)和通过 API 服务发布两种方式
|
||||
|
||||
使用方法:
|
||||
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png card_2.png
|
||||
# 直接发布(使用本地签名)
|
||||
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png
|
||||
|
||||
# 通过 API 服务发布
|
||||
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png --api-mode
|
||||
|
||||
环境变量:
|
||||
在同目录下创建 .env 文件,配置 XHS_COOKIE:
|
||||
在同目录或项目根目录下创建 .env 文件,配置:
|
||||
|
||||
# 必需:小红书 Cookie
|
||||
XHS_COOKIE=your_cookie_string_here
|
||||
|
||||
# 可选:API 服务地址(使用 --api-mode 时需要)
|
||||
XHS_API_URL=http://localhost:5005
|
||||
|
||||
依赖安装:
|
||||
pip install xhs python-dotenv
|
||||
pip install xhs python-dotenv requests
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
from xhs import XhsClient
|
||||
import requests
|
||||
except ImportError as e:
|
||||
print(f"缺少依赖: {e}")
|
||||
print("请运行: pip install xhs python-dotenv")
|
||||
print("请运行: pip install python-dotenv requests")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_cookie():
|
||||
def load_cookie() -> str:
|
||||
"""从 .env 文件加载 Cookie"""
|
||||
# 尝试从当前目录加载 .env
|
||||
env_path = Path.cwd() / '.env'
|
||||
if env_path.exists():
|
||||
load_dotenv(env_path)
|
||||
# 尝试从多个位置加载 .env
|
||||
env_paths = [
|
||||
Path.cwd() / '.env',
|
||||
Path(__file__).parent.parent / '.env',
|
||||
Path(__file__).parent.parent.parent / '.env',
|
||||
]
|
||||
|
||||
# 也尝试从脚本目录加载
|
||||
script_env = Path(__file__).parent.parent / '.env'
|
||||
if script_env.exists():
|
||||
load_dotenv(script_env)
|
||||
for env_path in env_paths:
|
||||
if env_path.exists():
|
||||
load_dotenv(env_path)
|
||||
break
|
||||
|
||||
cookie = os.getenv('XHS_COOKIE')
|
||||
if not cookie:
|
||||
print("❌ 错误: 未找到 XHS_COOKIE 环境变量")
|
||||
print("请在当前目录创建 .env 文件,添加以下内容:")
|
||||
print("请创建 .env 文件,添加以下内容:")
|
||||
print("XHS_COOKIE=your_cookie_string_here")
|
||||
print("\nCookie 获取方式:")
|
||||
print("1. 在浏览器中登录小红书(https://www.xiaohongshu.com)")
|
||||
print("2. 打开开发者工具(F12)")
|
||||
print("3. 在 Network 标签中查看任意请求的 Cookie 头")
|
||||
print("4. 复制完整的 cookie 字符串")
|
||||
sys.exit(1)
|
||||
|
||||
return cookie
|
||||
|
||||
|
||||
def create_client(cookie: str) -> XhsClient:
|
||||
"""创建小红书客户端"""
|
||||
try:
|
||||
# 使用本地签名
|
||||
from xhs.help import sign as local_sign
|
||||
|
||||
def sign_func(uri, data=None, a1="", web_session=""):
|
||||
return local_sign(uri, data, a1=a1)
|
||||
|
||||
client = XhsClient(cookie=cookie, sign=sign_func)
|
||||
return client
|
||||
except Exception as e:
|
||||
print(f"❌ 创建客户端失败: {e}")
|
||||
sys.exit(1)
|
||||
def parse_cookie(cookie_string: str) -> Dict[str, str]:
|
||||
"""解析 Cookie 字符串为字典"""
|
||||
cookies = {}
|
||||
for item in cookie_string.split(';'):
|
||||
item = item.strip()
|
||||
if '=' in item:
|
||||
key, value = item.split('=', 1)
|
||||
cookies[key.strip()] = value.strip()
|
||||
return cookies
|
||||
|
||||
|
||||
def validate_images(image_paths: list) -> list:
|
||||
def validate_cookie(cookie_string: str) -> bool:
|
||||
"""验证 Cookie 是否包含必要的字段"""
|
||||
cookies = parse_cookie(cookie_string)
|
||||
|
||||
# 检查必需的 cookie 字段
|
||||
required_fields = ['a1', 'web_session']
|
||||
missing = [f for f in required_fields if f not in cookies]
|
||||
|
||||
if missing:
|
||||
print(f"⚠️ Cookie 可能不完整,缺少字段: {', '.join(missing)}")
|
||||
print("这可能导致签名失败,请确保 Cookie 包含 a1 和 web_session 字段")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_api_url() -> str:
|
||||
"""获取 API 服务地址"""
|
||||
return os.getenv('XHS_API_URL', 'http://localhost:5005')
|
||||
|
||||
|
||||
def validate_images(image_paths: List[str]) -> List[str]:
|
||||
"""验证图片文件是否存在"""
|
||||
valid_images = []
|
||||
for path in image_paths:
|
||||
@@ -82,51 +117,218 @@ def validate_images(image_paths: list) -> list:
|
||||
return valid_images
|
||||
|
||||
|
||||
def publish_note(client: XhsClient, title: str, desc: str, images: list,
|
||||
is_private: bool = False, post_time: str = None):
|
||||
"""发布图文笔记"""
|
||||
try:
|
||||
print(f"\n🚀 准备发布笔记...")
|
||||
class LocalPublisher:
|
||||
"""本地发布模式:直接使用 xhs 库"""
|
||||
|
||||
def __init__(self, cookie: str):
|
||||
self.cookie = cookie
|
||||
self.client = None
|
||||
|
||||
def init_client(self):
|
||||
"""初始化 xhs 客户端"""
|
||||
try:
|
||||
from xhs import XhsClient
|
||||
from xhs.help import sign as local_sign
|
||||
except ImportError:
|
||||
print("❌ 错误: 缺少 xhs 库")
|
||||
print("请运行: pip install xhs")
|
||||
sys.exit(1)
|
||||
|
||||
# 解析 a1 值
|
||||
cookies = parse_cookie(self.cookie)
|
||||
a1 = cookies.get('a1', '')
|
||||
|
||||
def sign_func(uri, data=None, a1_param="", web_session=""):
|
||||
# 使用 cookie 中的 a1 值
|
||||
return local_sign(uri, data, a1=a1 or a1_param)
|
||||
|
||||
self.client = XhsClient(cookie=self.cookie, sign=sign_func)
|
||||
|
||||
def get_user_info(self) -> Optional[Dict[str, Any]]:
|
||||
"""获取当前登录用户信息"""
|
||||
try:
|
||||
info = self.client.get_self_info()
|
||||
print(f"👤 当前用户: {info.get('nickname', '未知')}")
|
||||
return info
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法获取用户信息: {e}")
|
||||
return None
|
||||
|
||||
def publish(self, title: str, desc: str, images: List[str],
|
||||
is_private: bool = False, post_time: str = None) -> Dict[str, Any]:
|
||||
"""发布图文笔记"""
|
||||
print(f"\n🚀 准备发布笔记(本地模式)...")
|
||||
print(f" 📌 标题: {title}")
|
||||
print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
|
||||
print(f" 🖼️ 图片数量: {len(images)}")
|
||||
|
||||
result = client.create_image_note(
|
||||
title=title,
|
||||
desc=desc,
|
||||
files=images,
|
||||
is_private=is_private,
|
||||
post_time=post_time
|
||||
)
|
||||
|
||||
print("\n✨ 笔记发布成功!")
|
||||
if isinstance(result, dict):
|
||||
note_id = result.get('note_id') or result.get('id')
|
||||
if note_id:
|
||||
print(f" 📎 笔记ID: {note_id}")
|
||||
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发布失败: {e}")
|
||||
sys.exit(1)
|
||||
try:
|
||||
result = self.client.create_image_note(
|
||||
title=title,
|
||||
desc=desc,
|
||||
files=images,
|
||||
is_private=is_private,
|
||||
post_time=post_time
|
||||
)
|
||||
|
||||
print("\n✨ 笔记发布成功!")
|
||||
if isinstance(result, dict):
|
||||
note_id = result.get('note_id') or result.get('id')
|
||||
if note_id:
|
||||
print(f" 📎 笔记ID: {note_id}")
|
||||
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
print(f"\n❌ 发布失败: {error_msg}")
|
||||
|
||||
# 提供具体的错误排查建议
|
||||
if 'sign' in error_msg.lower() or 'signature' in error_msg.lower():
|
||||
print("\n💡 签名错误排查建议:")
|
||||
print("1. 确保 Cookie 包含有效的 a1 和 web_session 字段")
|
||||
print("2. Cookie 可能已过期,请重新获取")
|
||||
print("3. 尝试使用 --api-mode 通过 API 服务发布")
|
||||
elif 'cookie' in error_msg.lower():
|
||||
print("\n💡 Cookie 错误排查建议:")
|
||||
print("1. 确保 Cookie 格式正确")
|
||||
print("2. Cookie 可能已过期,请重新获取")
|
||||
print("3. 确保 Cookie 来自已登录的小红书网页版")
|
||||
|
||||
raise
|
||||
|
||||
|
||||
def get_user_info(client: XhsClient):
|
||||
"""获取当前登录用户信息"""
|
||||
try:
|
||||
info = client.get_self_info()
|
||||
print(f"\n👤 当前用户: {info.get('nickname', '未知')}")
|
||||
return info
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法获取用户信息: {e}")
|
||||
return None
|
||||
class ApiPublisher:
|
||||
"""API 发布模式:通过 xhs-api 服务发布"""
|
||||
|
||||
def __init__(self, cookie: str, api_url: str = None):
|
||||
self.cookie = cookie
|
||||
self.api_url = api_url or get_api_url()
|
||||
self.session_id = 'md2redbook_session'
|
||||
|
||||
def init_client(self):
|
||||
"""初始化 API 客户端"""
|
||||
print(f"📡 连接 API 服务: {self.api_url}")
|
||||
|
||||
# 健康检查
|
||||
try:
|
||||
resp = requests.get(f"{self.api_url}/health", timeout=5)
|
||||
if resp.status_code != 200:
|
||||
raise Exception("API 服务不可用")
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ 无法连接到 API 服务: {e}")
|
||||
print(f"\n💡 请确保 xhs-api 服务已启动:")
|
||||
print(f" cd xhs-api && python app_full.py")
|
||||
sys.exit(1)
|
||||
|
||||
# 初始化 session
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{self.api_url}/init",
|
||||
json={
|
||||
"session_id": self.session_id,
|
||||
"cookie": self.cookie
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
result = resp.json()
|
||||
|
||||
if resp.status_code == 200 and result.get('status') == 'success':
|
||||
print(f"✅ API 初始化成功")
|
||||
user_info = result.get('user_info', {})
|
||||
if user_info:
|
||||
print(f"👤 当前用户: {user_info.get('nickname', '未知')}")
|
||||
elif result.get('status') == 'warning':
|
||||
print(f"⚠️ {result.get('message')}")
|
||||
else:
|
||||
raise Exception(result.get('error', '初始化失败'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ API 初始化失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def get_user_info(self) -> Optional[Dict[str, Any]]:
|
||||
"""获取当前登录用户信息"""
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{self.api_url}/user/info",
|
||||
json={"session_id": self.session_id},
|
||||
timeout=10
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
result = resp.json()
|
||||
if result.get('status') == 'success':
|
||||
info = result.get('user_info', {})
|
||||
print(f"👤 当前用户: {info.get('nickname', '未知')}")
|
||||
return info
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"⚠️ 无法获取用户信息: {e}")
|
||||
return None
|
||||
|
||||
def publish(self, title: str, desc: str, images: List[str],
|
||||
is_private: bool = False, post_time: str = None) -> Dict[str, Any]:
|
||||
"""发布图文笔记"""
|
||||
print(f"\n🚀 准备发布笔记(API 模式)...")
|
||||
print(f" 📌 标题: {title}")
|
||||
print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
|
||||
print(f" 🖼️ 图片数量: {len(images)}")
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"session_id": self.session_id,
|
||||
"title": title,
|
||||
"desc": desc,
|
||||
"files": images,
|
||||
"is_private": is_private
|
||||
}
|
||||
if post_time:
|
||||
payload["post_time"] = post_time
|
||||
|
||||
resp = requests.post(
|
||||
f"{self.api_url}/publish/image",
|
||||
json=payload,
|
||||
timeout=120
|
||||
)
|
||||
result = resp.json()
|
||||
|
||||
if resp.status_code == 200 and result.get('status') == 'success':
|
||||
print("\n✨ 笔记发布成功!")
|
||||
publish_result = result.get('result', {})
|
||||
if isinstance(publish_result, dict):
|
||||
note_id = publish_result.get('note_id') or publish_result.get('id')
|
||||
if note_id:
|
||||
print(f" 📎 笔记ID: {note_id}")
|
||||
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
|
||||
return publish_result
|
||||
else:
|
||||
raise Exception(result.get('error', '发布失败'))
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
print(f"\n❌ 发布失败: {error_msg}")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='将图片发布为小红书笔记'
|
||||
description='将图片发布为小红书笔记',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
示例:
|
||||
# 基本用法
|
||||
python publish_xhs.py -t "我的标题" -d "正文内容" -i cover.png card_1.png card_2.png
|
||||
|
||||
# 使用 API 模式
|
||||
python publish_xhs.py -t "我的标题" -d "正文内容" -i *.png --api-mode
|
||||
|
||||
# 设为私密笔记
|
||||
python publish_xhs.py -t "我的标题" -d "正文内容" -i *.png --private
|
||||
|
||||
# 定时发布
|
||||
python publish_xhs.py -t "我的标题" -d "正文内容" -i *.png --post-time "2024-12-01 10:00:00"
|
||||
'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'--title', '-t',
|
||||
@@ -154,6 +356,16 @@ def main():
|
||||
default=None,
|
||||
help='定时发布时间(格式:2024-01-01 12:00:00)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-mode',
|
||||
action='store_true',
|
||||
help='使用 API 模式发布(需要 xhs-api 服务运行)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-url',
|
||||
default=None,
|
||||
help='API 服务地址(默认: http://localhost:5005)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
@@ -170,32 +382,43 @@ def main():
|
||||
# 加载 Cookie
|
||||
cookie = load_cookie()
|
||||
|
||||
# 验证 Cookie 格式
|
||||
validate_cookie(cookie)
|
||||
|
||||
# 验证图片
|
||||
valid_images = validate_images(args.images)
|
||||
|
||||
# 创建客户端
|
||||
client = create_client(cookie)
|
||||
|
||||
# 获取用户信息(验证 Cookie 有效性)
|
||||
get_user_info(client)
|
||||
|
||||
if args.dry_run:
|
||||
print("\n🔍 验证模式 - 不会实际发布")
|
||||
print(f" 📌 标题: {args.title}")
|
||||
print(f" 📝 描述: {args.desc}")
|
||||
print(f" 🖼️ 图片: {valid_images}")
|
||||
print(f" 🔒 私密: {args.private}")
|
||||
print(f" ⏰ 定时: {args.post_time or '立即发布'}")
|
||||
print(f" 📡 模式: {'API' if args.api_mode else '本地'}")
|
||||
print("\n✅ 验证通过,可以发布")
|
||||
return
|
||||
|
||||
# 选择发布方式
|
||||
if args.api_mode:
|
||||
publisher = ApiPublisher(cookie, args.api_url)
|
||||
else:
|
||||
publisher = LocalPublisher(cookie)
|
||||
|
||||
# 初始化客户端
|
||||
publisher.init_client()
|
||||
|
||||
# 发布笔记
|
||||
publish_note(
|
||||
client=client,
|
||||
title=args.title,
|
||||
desc=args.desc,
|
||||
images=valid_images,
|
||||
is_private=args.private,
|
||||
post_time=args.post_time
|
||||
)
|
||||
try:
|
||||
publisher.publish(
|
||||
title=args.title,
|
||||
desc=args.desc,
|
||||
images=valid_images,
|
||||
is_private=args.private,
|
||||
post_time=args.post_time
|
||||
)
|
||||
except Exception as e:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,188 +1,511 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 小红书卡片渲染脚本 - Node.js 版本
|
||||
* 将 Markdown 文件渲染为小红书风格的图片卡片
|
||||
* 小红书卡片渲染脚本 - Node.js 增强版
|
||||
* 支持多种排版样式和智能分页策略
|
||||
*
|
||||
* 使用方法:
|
||||
* node render_xhs.js <markdown_file> [--output-dir <output_directory>]
|
||||
* node render_xhs.js <markdown_file> [options]
|
||||
*
|
||||
* 选项:
|
||||
* --output-dir, -o 输出目录(默认为当前工作目录)
|
||||
* --theme, -t 排版主题:default, playful-geometric, neo-brutalism, 等
|
||||
* --mode, -m 分页模式:separator, auto-fit, auto-split, dynamic
|
||||
* --width, -w 图片宽度(默认 1080)
|
||||
* --height, -h 图片高度(默认 1440)
|
||||
* --dpr 设备像素比(默认 2)
|
||||
*
|
||||
* 依赖安装:
|
||||
* npm install marked js-yaml playwright
|
||||
* npx playwright install chromium
|
||||
* npm install marked 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 yaml = require('yaml');
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
// 获取脚本所在目录
|
||||
const SCRIPT_DIR = path.dirname(__dirname);
|
||||
const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');
|
||||
const THEMES_DIR = path.join(ASSETS_DIR, 'themes');
|
||||
|
||||
// 卡片尺寸配置 (3:4 比例)
|
||||
const CARD_WIDTH = 1080;
|
||||
const CARD_HEIGHT = 1440;
|
||||
// 默认卡片尺寸配置 (3:4 比例)
|
||||
const DEFAULT_WIDTH = 1080;
|
||||
const DEFAULT_HEIGHT = 1440;
|
||||
const MAX_HEIGHT = 2160;
|
||||
|
||||
// 可用主题列表
|
||||
const AVAILABLE_THEMES = [
|
||||
'default',
|
||||
'playful-geometric',
|
||||
'neo-brutalism',
|
||||
'botanical',
|
||||
'professional',
|
||||
'retro',
|
||||
'terminal',
|
||||
'sketch'
|
||||
];
|
||||
|
||||
// 分页模式
|
||||
const PAGING_MODES = ['separator', 'auto-fit', 'auto-split', 'dynamic'];
|
||||
|
||||
// 主题背景色
|
||||
const THEME_BACKGROUNDS = {
|
||||
'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
|
||||
'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',
|
||||
'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',
|
||||
'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',
|
||||
'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',
|
||||
'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',
|
||||
'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',
|
||||
'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'
|
||||
};
|
||||
|
||||
// 封面标题文字渐变(随主题变化)
|
||||
const THEME_TITLE_GRADIENTS = {
|
||||
'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',
|
||||
'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',
|
||||
'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',
|
||||
'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',
|
||||
'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',
|
||||
'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',
|
||||
'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',
|
||||
'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 Markdown 文件,提取 YAML 头部和正文内容
|
||||
* 解析命令行参数
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
markdownFile: null,
|
||||
outputDir: process.cwd(),
|
||||
theme: 'default',
|
||||
mode: 'separator',
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
maxHeight: MAX_HEIGHT,
|
||||
dpr: 2
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const nextArg = args[i + 1];
|
||||
|
||||
switch (arg) {
|
||||
case '--output-dir':
|
||||
case '-o':
|
||||
options.outputDir = nextArg;
|
||||
i++;
|
||||
break;
|
||||
case '--theme':
|
||||
case '-t':
|
||||
options.theme = nextArg;
|
||||
i++;
|
||||
break;
|
||||
case '--mode':
|
||||
case '-m':
|
||||
options.mode = nextArg;
|
||||
i++;
|
||||
break;
|
||||
case '--width':
|
||||
case '-w':
|
||||
options.width = parseInt(nextArg);
|
||||
i++;
|
||||
break;
|
||||
case '--height':
|
||||
options.height = parseInt(nextArg);
|
||||
i++;
|
||||
break;
|
||||
case '--max-height':
|
||||
options.maxHeight = parseInt(nextArg);
|
||||
i++;
|
||||
break;
|
||||
case '--dpr':
|
||||
options.dpr = parseInt(nextArg);
|
||||
i++;
|
||||
break;
|
||||
case '--help':
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
default:
|
||||
if (!arg.startsWith('-')) {
|
||||
options.markdownFile = arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印帮助信息
|
||||
*/
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
小红书卡片渲染脚本 - Node.js 版本
|
||||
|
||||
使用方法:
|
||||
node render_xhs.js <markdown_file> [options]
|
||||
|
||||
选项:
|
||||
--output-dir, -o 输出目录(默认为当前工作目录)
|
||||
--theme, -t 排版主题
|
||||
--mode, -m 分页模式
|
||||
--width, -w 图片宽度(默认 1080)
|
||||
--height 图片高度(默认 1440)
|
||||
--max-height 最大高度(默认 2160)
|
||||
--dpr 设备像素比(默认 2)
|
||||
|
||||
可用主题: ${AVAILABLE_THEMES.join(', ')}
|
||||
分页模式: ${PAGING_MODES.join(', ')}
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Markdown 文件
|
||||
*/
|
||||
function parseMarkdownFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 解析 YAML 头部
|
||||
const yamlPattern = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
||||
const yamlMatch = content.match(yamlPattern);
|
||||
const yamlMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
||||
|
||||
let metadata = {};
|
||||
let body = content;
|
||||
|
||||
if (yamlMatch) {
|
||||
try {
|
||||
metadata = yaml.load(yamlMatch[1]) || {};
|
||||
metadata = yaml.parse(yamlMatch[1]) || {};
|
||||
} catch (e) {
|
||||
metadata = {};
|
||||
}
|
||||
body = content.slice(yamlMatch[0].length);
|
||||
}
|
||||
|
||||
return {
|
||||
metadata,
|
||||
body: body.trim()
|
||||
};
|
||||
return { metadata, body: body.trim() };
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照 --- 分隔符拆分正文为多张卡片内容
|
||||
* 按分隔符拆分内容
|
||||
*/
|
||||
function splitContentBySeparator(body) {
|
||||
const parts = body.split(/\n---+\n/);
|
||||
return parts.filter(part => part.trim()).map(part => part.trim());
|
||||
return parts.map(p => p.trim()).filter(p => p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Markdown 转换为 HTML
|
||||
* 加载主题 CSS
|
||||
*/
|
||||
function convertMarkdownToHtml(mdContent) {
|
||||
// 处理 tags(以 # 开头的标签)
|
||||
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) {
|
||||
tagsHtml = '<div class="tags-container">';
|
||||
for (const tag of tags) {
|
||||
tagsHtml += `<span class="tag">${tag}</span>`;
|
||||
}
|
||||
tagsHtml += '</div>';
|
||||
}
|
||||
function loadThemeCss(theme) {
|
||||
const themeFile = path.join(THEMES_DIR, `${theme}.css`);
|
||||
if (fs.existsSync(themeFile)) {
|
||||
return fs.readFileSync(themeFile, 'utf-8');
|
||||
}
|
||||
|
||||
// 转换 Markdown 为 HTML
|
||||
const html = marked.parse(mdContent, {
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
return html + tagsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 HTML 模板
|
||||
*/
|
||||
function loadTemplate(templateName) {
|
||||
const templatePath = path.join(ASSETS_DIR, templateName);
|
||||
return fs.readFileSync(templatePath, 'utf-8');
|
||||
const defaultFile = path.join(THEMES_DIR, 'default.css');
|
||||
if (fs.existsSync(defaultFile)) {
|
||||
return fs.readFileSync(defaultFile, 'utf-8');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成封面 HTML
|
||||
*/
|
||||
function generateCoverHtml(metadata) {
|
||||
let template = loadTemplate('cover.html');
|
||||
|
||||
let emoji = metadata.emoji || '📝';
|
||||
function generateCoverHtml(metadata, theme, width, height) {
|
||||
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);
|
||||
}
|
||||
if (title.length > 15) title = title.slice(0, 15);
|
||||
if (subtitle.length > 15) subtitle = subtitle.slice(0, 15);
|
||||
|
||||
template = template.replace('{{EMOJI}}', emoji);
|
||||
template = template.replace('{{TITLE}}', title);
|
||||
template = template.replace('{{SUBTITLE}}', subtitle);
|
||||
const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
|
||||
const titleBg = THEME_TITLE_GRADIENTS[theme] || THEME_TITLE_GRADIENTS['default'];
|
||||
|
||||
return template;
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=${width}, height=${height}">
|
||||
<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: ${width}px;
|
||||
height: ${height}px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
background: ${bg};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-inner {
|
||||
position: absolute;
|
||||
width: ${Math.floor(width * 0.88)}px;
|
||||
height: ${Math.floor(height * 0.91)}px;
|
||||
left: ${Math.floor(width * 0.06)}px;
|
||||
top: ${Math.floor(height * 0.045)}px;
|
||||
background: #F3F3F3;
|
||||
border-radius: 25px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${Math.floor(width * 0.074)}px ${Math.floor(width * 0.079)}px;
|
||||
}
|
||||
|
||||
.cover-emoji {
|
||||
font-size: ${Math.floor(width * 0.167)}px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: ${Math.floor(height * 0.035)}px;
|
||||
}
|
||||
|
||||
.cover-title {
|
||||
font-weight: 900;
|
||||
font-size: ${Math.floor(width * 0.12)}px;
|
||||
line-height: 1.4;
|
||||
background: ${titleBg};
|
||||
-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: ${Math.floor(width * 0.067)}px;
|
||||
line-height: 1.4;
|
||||
color: #000000;
|
||||
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) {
|
||||
let template = loadTemplate('card.html');
|
||||
|
||||
const htmlContent = convertMarkdownToHtml(content);
|
||||
function generateCardHtml(content, theme, pageNumber, totalPages, width, height, mode) {
|
||||
const htmlContent = marked.parse(content);
|
||||
const themeCss = loadThemeCss(theme);
|
||||
const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';
|
||||
const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
|
||||
|
||||
template = template.replace('{{CONTENT}}', htmlContent);
|
||||
template = template.replace('{{PAGE_NUMBER}}', pageText);
|
||||
let containerStyle, innerStyle, contentStyle;
|
||||
|
||||
return template;
|
||||
if (mode === 'auto-fit') {
|
||||
containerStyle = `
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
background: ${bg};
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
innerStyle = `
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
height: calc(${height}px - 100px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
contentStyle = 'flex: 1; overflow: hidden;';
|
||||
} else if (mode === 'dynamic') {
|
||||
containerStyle = `
|
||||
width: ${width}px;
|
||||
min-height: ${height}px;
|
||||
background: ${bg};
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
`;
|
||||
innerStyle = `
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
`;
|
||||
contentStyle = '';
|
||||
} else {
|
||||
containerStyle = `
|
||||
width: ${width}px;
|
||||
min-height: ${height}px;
|
||||
background: ${bg};
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
innerStyle = `
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
min-height: calc(${height}px - 100px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
`;
|
||||
contentStyle = '';
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=${width}">
|
||||
<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: ${width}px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card-container { ${containerStyle} }
|
||||
.card-inner { ${innerStyle} }
|
||||
.card-content { line-height: 1.7; ${contentStyle} }
|
||||
/* auto-fit 用:对整个内容块做 transform 缩放 */
|
||||
.card-content-scale { transform-origin: top left; will-change: transform; }
|
||||
|
||||
${themeCss}
|
||||
|
||||
.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">
|
||||
<div class="card-content-scale">
|
||||
${htmlContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">${pageText}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Playwright 将 HTML 渲染为图片
|
||||
* 渲染 HTML 为图片
|
||||
*/
|
||||
async function renderHtmlToImage(htmlContent, outputPath, width = CARD_WIDTH, height = CARD_HEIGHT) {
|
||||
async function renderHtmlToImage(htmlContent, outputPath, width, height, mode, maxHeight, dpr) {
|
||||
const browser = await chromium.launch();
|
||||
const viewportHeight = mode !== 'dynamic' ? height : maxHeight;
|
||||
const page = await browser.newPage({
|
||||
viewport: { width, height }
|
||||
viewport: { width, height: viewportHeight },
|
||||
deviceScaleFactor: dpr
|
||||
});
|
||||
|
||||
// 设置 HTML 内容
|
||||
await page.setContent(htmlContent, {
|
||||
waitUntil: 'networkidle'
|
||||
});
|
||||
|
||||
// 等待字体加载
|
||||
await page.setContent(htmlContent);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 获取实际内容高度
|
||||
const contentHeight = await page.evaluate(() => {
|
||||
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
});
|
||||
let actualHeight;
|
||||
|
||||
// 确保高度至少为 1440px(3:4 比例)
|
||||
const actualHeight = Math.max(height, contentHeight);
|
||||
if (mode === 'auto-fit') {
|
||||
await page.evaluate(() => {
|
||||
const viewportContent = document.querySelector('.card-content');
|
||||
const scaleEl = document.querySelector('.card-content-scale');
|
||||
if (!viewportContent || !scaleEl) return;
|
||||
|
||||
// reset
|
||||
scaleEl.style.transform = 'none';
|
||||
scaleEl.style.width = '';
|
||||
scaleEl.style.height = '';
|
||||
|
||||
const availableWidth = viewportContent.clientWidth;
|
||||
const availableHeight = viewportContent.clientHeight;
|
||||
|
||||
const rect = scaleEl.getBoundingClientRect();
|
||||
const contentWidth = Math.max(scaleEl.scrollWidth, rect.width);
|
||||
const contentHeight = Math.max(scaleEl.scrollHeight, rect.height);
|
||||
|
||||
if (!contentWidth || !contentHeight || !availableWidth || !availableHeight) return;
|
||||
|
||||
const scale = Math.min(1, availableWidth / contentWidth, availableHeight / contentHeight);
|
||||
|
||||
// expand layout box to avoid clip
|
||||
scaleEl.style.width = (availableWidth / scale) + 'px';
|
||||
|
||||
scaleEl.style.transformOrigin = 'top left';
|
||||
scaleEl.style.transform = `translate(0px, 0px) scale(${scale})`;
|
||||
});
|
||||
await page.waitForTimeout(100);
|
||||
actualHeight = height;
|
||||
} else if (mode === 'dynamic') {
|
||||
const contentHeight = await page.evaluate(() => {
|
||||
const container = document.querySelector('.card-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
});
|
||||
actualHeight = Math.max(height, Math.min(contentHeight, maxHeight));
|
||||
} else {
|
||||
const contentHeight = await page.evaluate(() => {
|
||||
const container = document.querySelector('.card-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
});
|
||||
actualHeight = Math.max(height, contentHeight);
|
||||
}
|
||||
|
||||
// 截图
|
||||
await page.screenshot({
|
||||
path: outputPath,
|
||||
clip: { x: 0, y: 0, width, height: actualHeight },
|
||||
type: 'png'
|
||||
});
|
||||
|
||||
console.log(` ✅ 已生成: ${outputPath}`);
|
||||
|
||||
await browser.close();
|
||||
console.log(` ✅ 已生成: ${outputPath} (${width}x${actualHeight})`);
|
||||
return actualHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主渲染函数:将 Markdown 文件渲染为多张卡片图片
|
||||
* 主渲染函数
|
||||
*/
|
||||
async function renderMarkdownToCards(mdFile, outputDir) {
|
||||
console.log(`\n🎨 开始渲染: ${mdFile}`);
|
||||
async function renderMarkdownToCards(options) {
|
||||
const { markdownFile, outputDir, theme, mode, width, height, maxHeight, dpr } = options;
|
||||
|
||||
console.log(`\n🎨 开始渲染: ${markdownFile}`);
|
||||
console.log(` 📐 主题: ${theme}`);
|
||||
console.log(` 📏 模式: ${mode}`);
|
||||
console.log(` 📐 尺寸: ${width}x${height}`);
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
@@ -190,10 +513,9 @@ async function renderMarkdownToCards(mdFile, outputDir) {
|
||||
}
|
||||
|
||||
// 解析 Markdown 文件
|
||||
const data = parseMarkdownFile(mdFile);
|
||||
const { metadata, body } = data;
|
||||
const { metadata, body } = parseMarkdownFile(markdownFile);
|
||||
|
||||
// 分割正文内容
|
||||
// 分割内容
|
||||
const cardContents = splitContentBySeparator(body);
|
||||
const totalCards = cardContents.length;
|
||||
|
||||
@@ -202,67 +524,53 @@ async function renderMarkdownToCards(mdFile, outputDir) {
|
||||
// 生成封面
|
||||
if (metadata.emoji || metadata.title) {
|
||||
console.log(' 📷 生成封面...');
|
||||
const coverHtml = generateCoverHtml(metadata);
|
||||
const coverHtml = generateCoverHtml(metadata, theme, width, height);
|
||||
const coverPath = path.join(outputDir, 'cover.png');
|
||||
await renderHtmlToImage(coverHtml, coverPath);
|
||||
await renderHtmlToImage(coverHtml, coverPath, width, height, 'separator', maxHeight, dpr);
|
||||
}
|
||||
|
||||
// 生成正文卡片
|
||||
for (let i = 0; i < cardContents.length; i++) {
|
||||
const pageNum = i + 1;
|
||||
console.log(` 📷 生成卡片 ${pageNum}/${totalCards}...`);
|
||||
const cardHtml = generateCardHtml(cardContents[i], pageNum, totalCards);
|
||||
const cardPath = path.join(outputDir, `card_${pageNum}.png`);
|
||||
await renderHtmlToImage(cardHtml, cardPath);
|
||||
const content = cardContents[i];
|
||||
console.log(` 📷 生成卡片 ${i + 1}/${totalCards}...`);
|
||||
const cardHtml = generateCardHtml(content, theme, i + 1, totalCards, width, height, mode);
|
||||
const cardPath = path.join(outputDir, `card_${i + 1}.png`);
|
||||
await renderHtmlToImage(cardHtml, cardPath, width, height, mode, maxHeight, dpr);
|
||||
}
|
||||
|
||||
console.log(`\n✨ 渲染完成!图片已保存到: ${outputDir}`);
|
||||
return totalCards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析命令行参数
|
||||
* 主函数
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('用法: node render_xhs.js <markdown_file> [--output-dir <output_directory>]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let markdownFile = null;
|
||||
let outputDir = process.cwd();
|
||||
|
||||
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].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 };
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
const { markdownFile, outputDir } = parseArgs();
|
||||
await renderMarkdownToCards(markdownFile, outputDir);
|
||||
const options = parseArgs();
|
||||
|
||||
if (!options.markdownFile) {
|
||||
console.error('❌ 错误: 请提供 Markdown 文件路径');
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(options.markdownFile)) {
|
||||
console.error(`❌ 错误: 文件不存在 - ${options.markdownFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!AVAILABLE_THEMES.includes(options.theme)) {
|
||||
console.error(`❌ 错误: 不支持的主题 - ${options.theme}`);
|
||||
console.error(`可用主题: ${AVAILABLE_THEMES.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!PAGING_MODES.includes(options.mode)) {
|
||||
console.error(`❌ 错误: 不支持的分页模式 - ${options.mode}`);
|
||||
console.error(`可用模式: ${PAGING_MODES.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await renderMarkdownToCards(options);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('❌ 渲染失败:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
main().catch(console.error);
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书卡片渲染脚本 - Python 版本
|
||||
将 Markdown 文件渲染为小红书风格的图片卡片
|
||||
小红书卡片渲染脚本 - 增强版
|
||||
支持多种排版样式和智能分页策略
|
||||
|
||||
使用方法:
|
||||
python render_xhs.py <markdown_file> [--output-dir <output_directory>]
|
||||
python render_xhs.py <markdown_file> [options]
|
||||
|
||||
选项:
|
||||
--output-dir, -o 输出目录(默认为当前工作目录)
|
||||
--theme, -t 排版主题:default, playful-geometric, neo-brutalism,
|
||||
botanical, professional, retro, terminal, sketch
|
||||
--mode, -m 分页模式:
|
||||
- separator : 按 --- 分隔符手动分页(默认)
|
||||
- auto-fit : 自动缩放文字以填满固定尺寸
|
||||
- auto-split : 根据内容高度自动切分
|
||||
- dynamic : 根据内容动态调整图片高度
|
||||
--width, -w 图片宽度(默认 1080)
|
||||
--height, -h 图片高度(默认 1440,dynamic 模式下为最小高度)
|
||||
--max-height dynamic 模式下的最大高度(默认 4320
|
||||
--dpr 设备像素比(默认 2)
|
||||
|
||||
依赖安装:
|
||||
pip install markdown pyyaml pillow playwright
|
||||
pip install markdown pyyaml playwright
|
||||
playwright install chromium
|
||||
"""
|
||||
|
||||
@@ -18,6 +32,7 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
try:
|
||||
import markdown
|
||||
@@ -32,10 +47,27 @@ except ImportError as e:
|
||||
# 获取脚本所在目录
|
||||
SCRIPT_DIR = Path(__file__).parent.parent
|
||||
ASSETS_DIR = SCRIPT_DIR / "assets"
|
||||
THEMES_DIR = ASSETS_DIR / "themes"
|
||||
|
||||
# 卡片尺寸配置 (3:4 比例)
|
||||
CARD_WIDTH = 1080
|
||||
CARD_HEIGHT = 1440
|
||||
# 默认卡片尺寸配置 (3:4 比例)
|
||||
DEFAULT_WIDTH = 1080
|
||||
DEFAULT_HEIGHT = 1440
|
||||
MAX_HEIGHT = 4320 # dynamic 模式最大高度
|
||||
|
||||
# 可用主题列表
|
||||
AVAILABLE_THEMES = [
|
||||
'default',
|
||||
'playful-geometric',
|
||||
'neo-brutalism',
|
||||
'botanical',
|
||||
'professional',
|
||||
'retro',
|
||||
'terminal',
|
||||
'sketch'
|
||||
]
|
||||
|
||||
# 分页模式
|
||||
PAGING_MODES = ['separator', 'auto-fit', 'auto-split', 'dynamic']
|
||||
|
||||
|
||||
def parse_markdown_file(file_path: str) -> dict:
|
||||
@@ -63,9 +95,8 @@ def parse_markdown_file(file_path: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def split_content_by_separator(body: str) -> list:
|
||||
def split_content_by_separator(body: str) -> List[str]:
|
||||
"""按照 --- 分隔符拆分正文为多张卡片内容"""
|
||||
# 使用 --- 作为分隔符,但要排除 YAML 头部的 ---
|
||||
parts = re.split(r'\n---+\n', body)
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
|
||||
@@ -96,17 +127,23 @@ def convert_markdown_to_html(md_content: str) -> str:
|
||||
return html + tags_html
|
||||
|
||||
|
||||
def load_template(template_name: str) -> str:
|
||||
"""加载 HTML 模板"""
|
||||
template_path = ASSETS_DIR / template_name
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
def load_theme_css(theme: str) -> str:
|
||||
"""加载主题 CSS 样式"""
|
||||
theme_file = THEMES_DIR / f"{theme}.css"
|
||||
if theme_file.exists():
|
||||
with open(theme_file, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
else:
|
||||
# 如果主题不存在,使用默认主题
|
||||
default_file = THEMES_DIR / "default.css"
|
||||
if default_file.exists():
|
||||
with open(default_file, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
return ""
|
||||
|
||||
|
||||
def generate_cover_html(metadata: dict) -> str:
|
||||
def generate_cover_html(metadata: dict, theme: str, width: int, height: int) -> str:
|
||||
"""生成封面 HTML"""
|
||||
template = load_template('cover.html')
|
||||
|
||||
emoji = metadata.get('emoji', '📝')
|
||||
title = metadata.get('title', '标题')
|
||||
subtitle = metadata.get('subtitle', '')
|
||||
@@ -117,32 +154,283 @@ def generate_cover_html(metadata: dict) -> str:
|
||||
if len(subtitle) > 15:
|
||||
subtitle = subtitle[:15]
|
||||
|
||||
html = template.replace('{{EMOJI}}', emoji)
|
||||
html = html.replace('{{TITLE}}', title)
|
||||
html = html.replace('{{SUBTITLE}}', subtitle)
|
||||
# 获取主题背景色
|
||||
theme_backgrounds = {
|
||||
'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
|
||||
'playful-geometric': 'linear-gradient(180deg, #8B5CF6 0%, #F472B6 100%)',
|
||||
'neo-brutalism': 'linear-gradient(180deg, #FF4757 0%, #FECA57 100%)',
|
||||
'botanical': 'linear-gradient(180deg, #4A7C59 0%, #8FBC8F 100%)',
|
||||
'professional': 'linear-gradient(180deg, #2563EB 0%, #3B82F6 100%)',
|
||||
'retro': 'linear-gradient(180deg, #D35400 0%, #F39C12 100%)',
|
||||
'terminal': 'linear-gradient(180deg, #0D1117 0%, #21262D 100%)',
|
||||
'sketch': 'linear-gradient(180deg, #555555 0%, #999999 100%)'
|
||||
}
|
||||
bg = theme_backgrounds.get(theme, theme_backgrounds['default'])
|
||||
|
||||
# 封面标题文字渐变随主题变化
|
||||
title_gradients = {
|
||||
'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',
|
||||
'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',
|
||||
'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',
|
||||
'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',
|
||||
'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',
|
||||
'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',
|
||||
'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',
|
||||
'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',
|
||||
}
|
||||
title_bg = title_gradients.get(theme, title_gradients['default'])
|
||||
|
||||
html = f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width={width}, height={height}">
|
||||
<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: {width}px;
|
||||
height: {height}px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.cover-container {{
|
||||
width: {width}px;
|
||||
height: {height}px;
|
||||
background: {bg};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.cover-inner {{
|
||||
position: absolute;
|
||||
width: {int(width * 0.88)}px;
|
||||
height: {int(height * 0.91)}px;
|
||||
left: {int(width * 0.06)}px;
|
||||
top: {int(height * 0.045)}px;
|
||||
background: #F3F3F3;
|
||||
border-radius: 25px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: {int(width * 0.074)}px {int(width * 0.079)}px;
|
||||
}}
|
||||
|
||||
.cover-emoji {{
|
||||
font-size: {int(width * 0.167)}px;
|
||||
line-height: 1.2;
|
||||
margin-bottom: {int(height * 0.035)}px;
|
||||
}}
|
||||
|
||||
.cover-title {{
|
||||
font-weight: 900;
|
||||
font-size: {int(width * 0.12)}px;
|
||||
line-height: 1.4;
|
||||
background: {title_bg};
|
||||
-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: {int(width * 0.067)}px;
|
||||
line-height: 1.4;
|
||||
color: #000000;
|
||||
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>'''
|
||||
return html
|
||||
|
||||
|
||||
def generate_card_html(content: str, page_number: int = 1, total_pages: int = 1) -> str:
|
||||
def generate_card_html(content: str, theme: str, page_number: int = 1,
|
||||
total_pages: int = 1, width: int = DEFAULT_WIDTH,
|
||||
height: int = DEFAULT_HEIGHT, mode: str = 'separator') -> str:
|
||||
"""生成正文卡片 HTML"""
|
||||
template = load_template('card.html')
|
||||
|
||||
html_content = convert_markdown_to_html(content)
|
||||
theme_css = load_theme_css(theme)
|
||||
|
||||
page_text = f"{page_number}/{total_pages}" if total_pages > 1 else ""
|
||||
|
||||
html = template.replace('{{CONTENT}}', html_content)
|
||||
html = html.replace('{{PAGE_NUMBER}}', page_text)
|
||||
# 获取主题背景色
|
||||
theme_backgrounds = {
|
||||
'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
|
||||
'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',
|
||||
'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',
|
||||
'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',
|
||||
'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',
|
||||
'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',
|
||||
'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',
|
||||
'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'
|
||||
}
|
||||
bg = theme_backgrounds.get(theme, theme_backgrounds['default'])
|
||||
|
||||
# 根据模式设置不同的容器样式
|
||||
if mode == 'auto-fit':
|
||||
container_style = f'''
|
||||
width: {width}px;
|
||||
height: {height}px;
|
||||
background: {bg};
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
overflow: hidden;
|
||||
'''
|
||||
inner_style = f'''
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
height: calc({height}px - 100px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
'''
|
||||
content_style = '''
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
'''
|
||||
elif mode == 'dynamic':
|
||||
container_style = f'''
|
||||
width: {width}px;
|
||||
min-height: {height}px;
|
||||
background: {bg};
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
'''
|
||||
inner_style = '''
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
'''
|
||||
content_style = ''
|
||||
else: # separator 和 auto-split
|
||||
container_style = f'''
|
||||
width: {width}px;
|
||||
min-height: {height}px;
|
||||
background: {bg};
|
||||
position: relative;
|
||||
padding: 50px;
|
||||
overflow: hidden;
|
||||
'''
|
||||
inner_style = f'''
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 60px;
|
||||
min-height: calc({height}px - 100px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
'''
|
||||
content_style = ''
|
||||
|
||||
html = f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width={width}">
|
||||
<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: {width}px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
}}
|
||||
|
||||
.card-container {{
|
||||
{container_style}
|
||||
}}
|
||||
|
||||
.card-inner {{
|
||||
{inner_style}
|
||||
}}
|
||||
|
||||
.card-content {{
|
||||
line-height: 1.7;
|
||||
{content_style}
|
||||
}}
|
||||
|
||||
/* auto-fit 用:对整个内容块做 transform 缩放 */
|
||||
.card-content-scale {{
|
||||
transform-origin: top left;
|
||||
will-change: transform;
|
||||
}}
|
||||
|
||||
{theme_css}
|
||||
|
||||
.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">
|
||||
<div class="card-content-scale">{html_content}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-number">{page_text}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
return html
|
||||
|
||||
|
||||
async def render_html_to_image(html_content: str, output_path: str, width: int = CARD_WIDTH, height: int = CARD_HEIGHT):
|
||||
async def render_html_to_image(html_content: str, output_path: str,
|
||||
width: int = DEFAULT_WIDTH,
|
||||
height: int = DEFAULT_HEIGHT,
|
||||
mode: str = 'separator',
|
||||
max_height: int = MAX_HEIGHT,
|
||||
dpr: int = 2):
|
||||
"""使用 Playwright 将 HTML 渲染为图片"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
page = await browser.new_page(viewport={'width': width, 'height': height})
|
||||
|
||||
# 设置视口大小
|
||||
viewport_height = height if mode != 'dynamic' else max_height
|
||||
page = await browser.new_page(
|
||||
viewport={'width': width, 'height': viewport_height},
|
||||
device_scale_factor=dpr
|
||||
)
|
||||
|
||||
# 创建临时 HTML 文件
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
||||
@@ -156,14 +444,59 @@ async def render_html_to_image(html_content: str, output_path: str, width: int =
|
||||
# 等待字体加载
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
# 获取实际内容高度
|
||||
content_height = await page.evaluate('''() => {
|
||||
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
}''')
|
||||
|
||||
# 确保高度至少为 1440px(3:4 比例)
|
||||
actual_height = max(height, content_height)
|
||||
if mode == 'auto-fit':
|
||||
# 自动缩放模式:对整个内容块做 transform 缩放(标题/代码块等固定 px 也会一起缩放)
|
||||
await page.evaluate('''() => {
|
||||
const viewportContent = document.querySelector('.card-content');
|
||||
const scaleEl = document.querySelector('.card-content-scale');
|
||||
if (!viewportContent || !scaleEl) return;
|
||||
|
||||
// 先重置,测量原始尺寸
|
||||
scaleEl.style.transform = 'none';
|
||||
scaleEl.style.width = '';
|
||||
scaleEl.style.height = '';
|
||||
|
||||
const availableWidth = viewportContent.clientWidth;
|
||||
const availableHeight = viewportContent.clientHeight;
|
||||
|
||||
// scrollWidth/scrollHeight 反映内容的自然尺寸
|
||||
const contentWidth = Math.max(scaleEl.scrollWidth, scaleEl.getBoundingClientRect().width);
|
||||
const contentHeight = Math.max(scaleEl.scrollHeight, scaleEl.getBoundingClientRect().height);
|
||||
|
||||
if (!contentWidth || !contentHeight || !availableWidth || !availableHeight) return;
|
||||
|
||||
// 只缩小不放大,避免“撑太大”
|
||||
const scale = Math.min(1, availableWidth / contentWidth, availableHeight / contentHeight);
|
||||
|
||||
// 为避免 transform 后布局尺寸不匹配导致裁切,扩大布局盒子
|
||||
scaleEl.style.width = (availableWidth / scale) + 'px';
|
||||
|
||||
// 顶部对齐更稳;如需居中可计算 offset
|
||||
const offsetX = 0;
|
||||
const offsetY = 0;
|
||||
|
||||
scaleEl.style.transformOrigin = 'top left';
|
||||
scaleEl.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
||||
}''')
|
||||
await page.wait_for_timeout(100)
|
||||
actual_height = height
|
||||
|
||||
elif mode == 'dynamic':
|
||||
# 动态高度模式:根据内容调整图片高度
|
||||
content_height = await page.evaluate('''() => {
|
||||
const container = document.querySelector('.card-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
}''')
|
||||
# 确保高度在合理范围内
|
||||
actual_height = max(height, min(content_height, max_height))
|
||||
|
||||
else: # separator 和 auto-split
|
||||
# 获取实际内容高度
|
||||
content_height = await page.evaluate('''() => {
|
||||
const container = document.querySelector('.card-container');
|
||||
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||
}''')
|
||||
actual_height = max(height, content_height)
|
||||
|
||||
# 截图
|
||||
await page.screenshot(
|
||||
@@ -172,16 +505,86 @@ async def render_html_to_image(html_content: str, output_path: str, width: int =
|
||||
type='png'
|
||||
)
|
||||
|
||||
print(f" ✅ 已生成: {output_path}")
|
||||
print(f" ✅ 已生成: {output_path} ({width}x{actual_height})")
|
||||
return actual_height
|
||||
|
||||
finally:
|
||||
os.unlink(temp_html_path)
|
||||
await browser.close()
|
||||
|
||||
|
||||
async def render_markdown_to_cards(md_file: str, output_dir: str):
|
||||
async def auto_split_content(body: str, theme: str, width: int, height: int,
|
||||
dpr: int = 2) -> List[str]:
|
||||
"""自动切分内容:根据渲染后的高度自动分页"""
|
||||
|
||||
# 将内容按段落分割
|
||||
paragraphs = re.split(r'\n\n+', body)
|
||||
|
||||
cards = []
|
||||
current_content = []
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
page = await browser.new_page(
|
||||
viewport={'width': width, 'height': height * 2},
|
||||
device_scale_factor=dpr
|
||||
)
|
||||
|
||||
try:
|
||||
for para in paragraphs:
|
||||
# 尝试将当前段落加入
|
||||
test_content = current_content + [para]
|
||||
test_md = '\n\n'.join(test_content)
|
||||
|
||||
html = generate_card_html(test_md, theme, 1, 1, width, height, 'auto-split')
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
temp_path = f.name
|
||||
|
||||
await page.goto(f'file://{temp_path}')
|
||||
await page.wait_for_load_state('networkidle')
|
||||
await page.wait_for_timeout(200)
|
||||
|
||||
content_height = await page.evaluate('''() => {
|
||||
const content = document.querySelector('.card-content');
|
||||
return content ? content.scrollHeight : 0;
|
||||
}''')
|
||||
|
||||
os.unlink(temp_path)
|
||||
|
||||
# 内容区域的可用高度(去除 padding 等)
|
||||
available_height = height - 220 # 50*2 padding + 60*2 inner padding
|
||||
|
||||
if content_height > available_height and current_content:
|
||||
# 当前卡片已满,保存并开始新卡片
|
||||
cards.append('\n\n'.join(current_content))
|
||||
current_content = [para]
|
||||
else:
|
||||
current_content = test_content
|
||||
|
||||
# 保存最后一张卡片
|
||||
if current_content:
|
||||
cards.append('\n\n'.join(current_content))
|
||||
|
||||
finally:
|
||||
await browser.close()
|
||||
|
||||
return cards
|
||||
|
||||
|
||||
async def render_markdown_to_cards(md_file: str, output_dir: str,
|
||||
theme: str = 'default',
|
||||
mode: str = 'separator',
|
||||
width: int = DEFAULT_WIDTH,
|
||||
height: int = DEFAULT_HEIGHT,
|
||||
max_height: int = MAX_HEIGHT,
|
||||
dpr: int = 2):
|
||||
"""主渲染函数:将 Markdown 文件渲染为多张卡片图片"""
|
||||
print(f"\n🎨 开始渲染: {md_file}")
|
||||
print(f" 📐 主题: {theme}")
|
||||
print(f" 📏 模式: {mode}")
|
||||
print(f" 📐 尺寸: {width}x{height}")
|
||||
|
||||
# 确保输出目录存在
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
@@ -191,25 +594,29 @@ async def render_markdown_to_cards(md_file: str, output_dir: str):
|
||||
metadata = data['metadata']
|
||||
body = data['body']
|
||||
|
||||
# 分割正文内容
|
||||
card_contents = split_content_by_separator(body)
|
||||
total_cards = len(card_contents)
|
||||
# 根据模式处理内容分割
|
||||
if mode == 'auto-split':
|
||||
print(" ⏳ 自动分析内容并切分...")
|
||||
card_contents = await auto_split_content(body, theme, width, height, dpr)
|
||||
else:
|
||||
card_contents = split_content_by_separator(body)
|
||||
|
||||
total_cards = len(card_contents)
|
||||
print(f" 📄 检测到 {total_cards} 张正文卡片")
|
||||
|
||||
# 生成封面
|
||||
if metadata.get('emoji') or metadata.get('title'):
|
||||
print(" 📷 生成封面...")
|
||||
cover_html = generate_cover_html(metadata)
|
||||
cover_html = generate_cover_html(metadata, theme, width, height)
|
||||
cover_path = os.path.join(output_dir, 'cover.png')
|
||||
await render_html_to_image(cover_html, cover_path)
|
||||
await render_html_to_image(cover_html, cover_path, width, height, 'separator', max_height, dpr)
|
||||
|
||||
# 生成正文卡片
|
||||
for i, content in enumerate(card_contents, 1):
|
||||
print(f" 📷 生成卡片 {i}/{total_cards}...")
|
||||
card_html = generate_card_html(content, i, total_cards)
|
||||
card_html = generate_card_html(content, theme, i, total_cards, width, height, mode)
|
||||
card_path = os.path.join(output_dir, f'card_{i}.png')
|
||||
await render_html_to_image(card_html, card_path)
|
||||
await render_html_to_image(card_html, card_path, width, height, mode, max_height, dpr)
|
||||
|
||||
print(f"\n✨ 渲染完成!图片已保存到: {output_dir}")
|
||||
return total_cards
|
||||
@@ -217,7 +624,25 @@ async def render_markdown_to_cards(md_file: str, output_dir: str):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='将 Markdown 文件渲染为小红书风格的图片卡片'
|
||||
description='将 Markdown 文件渲染为小红书风格的图片卡片(支持多种样式和分页模式)',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
可用主题:
|
||||
default - 默认紫色渐变风格
|
||||
playful-geometric - 活泼几何风格(Memphis 设计)
|
||||
neo-brutalism - 新粗野主义风格
|
||||
botanical - 植物园自然风格
|
||||
professional - 专业商务风格
|
||||
retro - 复古怀旧风格
|
||||
terminal - 终端/命令行风格
|
||||
sketch - 手绘素描风格
|
||||
|
||||
分页模式:
|
||||
separator - 按 --- 分隔符手动分页(默认)
|
||||
auto-fit - 自动缩放文字以填满固定尺寸
|
||||
auto-split - 根据内容高度自动切分
|
||||
dynamic - 根据内容动态调整图片高度
|
||||
'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'markdown_file',
|
||||
@@ -228,6 +653,42 @@ def main():
|
||||
default=os.getcwd(),
|
||||
help='输出目录(默认为当前工作目录)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--theme', '-t',
|
||||
choices=AVAILABLE_THEMES,
|
||||
default='default',
|
||||
help='排版主题(默认: default)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--mode', '-m',
|
||||
choices=PAGING_MODES,
|
||||
default='separator',
|
||||
help='分页模式(默认: separator)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--width', '-w',
|
||||
type=int,
|
||||
default=DEFAULT_WIDTH,
|
||||
help=f'图片宽度(默认: {DEFAULT_WIDTH})'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--height',
|
||||
type=int,
|
||||
default=DEFAULT_HEIGHT,
|
||||
help=f'图片高度(默认: {DEFAULT_HEIGHT})'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--max-height',
|
||||
type=int,
|
||||
default=MAX_HEIGHT,
|
||||
help=f'dynamic 模式下的最大高度(默认: {MAX_HEIGHT})'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dpr',
|
||||
type=int,
|
||||
default=2,
|
||||
help='设备像素比(默认: 2)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -235,7 +696,16 @@ def main():
|
||||
print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(render_markdown_to_cards(args.markdown_file, args.output_dir))
|
||||
asyncio.run(render_markdown_to_cards(
|
||||
args.markdown_file,
|
||||
args.output_dir,
|
||||
theme=args.theme,
|
||||
mode=args.mode,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
max_height=args.max_height,
|
||||
dpr=args.dpr
|
||||
))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||