diff --git a/README.md b/README.md index 9ec27bc..7183872 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,151 @@ -# 📕 Auto-Redbook-Skills +## 📕 Auto-Redbook-Skills(已重构版) -> 一个自动撰写笔记、生成图片、自动发布小红书的 Skills - -[![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://www.python.org/) -[![Node.js](https://img.shields.io/badge/Node.js-16+-green.svg)](https://nodejs.org/) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](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(活泼几何) -## 🚀 快速开始 +![Playful Geometric](demos/playful-geometric/card_1.png) -### Clone 项目 +### Retro(复古怀旧) -Clone 项目到本地 +![Retro](demos/retro/card_1.png) + +### Sketch(手绘素描) + +> 注意:该目录在 demos 中是大写 `Sketch` + +![Sketch](demos/Sketch/card_1.png) + +### Terminal(终端风格) + +![Terminal](demos/terminal/card_1.png) + +### Auto-fit 模式示例(自动缩放) + +![Auto Fit](demos/auto-fit/card_1.png) + +--- + +## 🚀 使用方式总览 + +### 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 diff --git a/SKILL.md b/SKILL.md index 5ca82a0..ef7ce0f 100644 --- a/SKILL.md +++ b/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 - -# 指定输出目录 -python scripts/render_xhs_v2.py -o - -# 指定样式主题 -python scripts/render_xhs_v2.py --style xiaohongshu - -# 查看所有可用样式 -python scripts/render_xhs_v2.py --list-styles +python scripts/render_xhs.py [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 +# 1) 默认主题 + 手动分隔分页 +python scripts/render_xhs.py content.md -m separator -# 指定输出目录和样式 -node scripts/render_xhs_v2.js -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 [--output-dir ] - -# Node.js 版本 -node scripts/render_xhs.js [--output-dir ] +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` diff --git a/assets/cover.html b/assets/cover.html index 57affca..96735ce 100644 --- a/assets/cover.html +++ b/assets/cover.html @@ -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; diff --git a/assets/example.md b/assets/example.md index db836b5..624e80e 100644 --- a/assets/example.md +++ b/assets/example.md @@ -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 +#小红书模板 #排版设计 #内容创作 diff --git a/assets/styles.css b/assets/styles.css index fa96494..7134fee 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -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 { diff --git a/assets/themes/botanical.css b/assets/themes/botanical.css new file mode 100644 index 0000000..1a1a86c --- /dev/null +++ b/assets/themes/botanical.css @@ -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; +} diff --git a/assets/themes/default.css b/assets/themes/default.css new file mode 100644 index 0000000..181c8d5 --- /dev/null +++ b/assets/themes/default.css @@ -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; +} diff --git a/assets/themes/neo-brutalism.css b/assets/themes/neo-brutalism.css new file mode 100644 index 0000000..5e415fb --- /dev/null +++ b/assets/themes/neo-brutalism.css @@ -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; +} diff --git a/assets/themes/playful-geometric.css b/assets/themes/playful-geometric.css new file mode 100644 index 0000000..3d0696a --- /dev/null +++ b/assets/themes/playful-geometric.css @@ -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; +} diff --git a/assets/themes/professional.css b/assets/themes/professional.css new file mode 100644 index 0000000..e628a55 --- /dev/null +++ b/assets/themes/professional.css @@ -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; +} diff --git a/assets/themes/retro.css b/assets/themes/retro.css new file mode 100644 index 0000000..51e7984 --- /dev/null +++ b/assets/themes/retro.css @@ -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; +} diff --git a/assets/themes/sketch.css b/assets/themes/sketch.css new file mode 100644 index 0000000..5f561fc --- /dev/null +++ b/assets/themes/sketch.css @@ -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); +} diff --git a/assets/themes/terminal.css b/assets/themes/terminal.css new file mode 100644 index 0000000..4bff8d3 --- /dev/null +++ b/assets/themes/terminal.css @@ -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; +} diff --git a/demos/.DS_Store b/demos/.DS_Store new file mode 100644 index 0000000..4d4a529 Binary files /dev/null and b/demos/.DS_Store differ diff --git a/demos/Sketch/card_1.png b/demos/Sketch/card_1.png new file mode 100644 index 0000000..a0a431f Binary files /dev/null and b/demos/Sketch/card_1.png differ diff --git a/demos/Sketch/card_2.png b/demos/Sketch/card_2.png new file mode 100644 index 0000000..cc0af71 Binary files /dev/null and b/demos/Sketch/card_2.png differ diff --git a/demos/Sketch/card_3.png b/demos/Sketch/card_3.png new file mode 100644 index 0000000..630dbc9 Binary files /dev/null and b/demos/Sketch/card_3.png differ diff --git a/demos/Sketch/card_4.png b/demos/Sketch/card_4.png new file mode 100644 index 0000000..81f7bdc Binary files /dev/null and b/demos/Sketch/card_4.png differ diff --git a/demos/Sketch/card_5.png b/demos/Sketch/card_5.png new file mode 100644 index 0000000..e512413 Binary files /dev/null and b/demos/Sketch/card_5.png differ diff --git a/demos/Sketch/cover.png b/demos/Sketch/cover.png new file mode 100644 index 0000000..46b2f4d Binary files /dev/null and b/demos/Sketch/cover.png differ diff --git a/demos/auto-fit/card_1.png b/demos/auto-fit/card_1.png new file mode 100644 index 0000000..d67c57e Binary files /dev/null and b/demos/auto-fit/card_1.png differ diff --git a/demos/auto-fit/cover.png b/demos/auto-fit/cover.png new file mode 100644 index 0000000..1a50e71 Binary files /dev/null and b/demos/auto-fit/cover.png differ diff --git a/demos/content.md b/demos/content.md new file mode 100644 index 0000000..ec68d73 --- /dev/null +++ b/demos/content.md @@ -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 工具支持 +- 可视化参数提示 +- 团队协作分享 diff --git a/demos/content_auto_fit.md b/demos/content_auto_fit.md new file mode 100644 index 0000000..b3926e3 --- /dev/null +++ b/demos/content_auto_fit.md @@ -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% 的专注力 diff --git a/demos/playful-geometric/card_1.png b/demos/playful-geometric/card_1.png new file mode 100644 index 0000000..d516b34 Binary files /dev/null and b/demos/playful-geometric/card_1.png differ diff --git a/demos/playful-geometric/card_2.png b/demos/playful-geometric/card_2.png new file mode 100644 index 0000000..f2af4cd Binary files /dev/null and b/demos/playful-geometric/card_2.png differ diff --git a/demos/playful-geometric/card_3.png b/demos/playful-geometric/card_3.png new file mode 100644 index 0000000..7ec6f68 Binary files /dev/null and b/demos/playful-geometric/card_3.png differ diff --git a/demos/playful-geometric/card_4.png b/demos/playful-geometric/card_4.png new file mode 100644 index 0000000..84f4e3f Binary files /dev/null and b/demos/playful-geometric/card_4.png differ diff --git a/demos/playful-geometric/card_5.png b/demos/playful-geometric/card_5.png new file mode 100644 index 0000000..145f1a2 Binary files /dev/null and b/demos/playful-geometric/card_5.png differ diff --git a/demos/playful-geometric/cover.png b/demos/playful-geometric/cover.png new file mode 100644 index 0000000..ce14a66 Binary files /dev/null and b/demos/playful-geometric/cover.png differ diff --git a/demos/retro/card_1.png b/demos/retro/card_1.png new file mode 100644 index 0000000..7c7c018 Binary files /dev/null and b/demos/retro/card_1.png differ diff --git a/demos/retro/card_2.png b/demos/retro/card_2.png new file mode 100644 index 0000000..8f7f413 Binary files /dev/null and b/demos/retro/card_2.png differ diff --git a/demos/retro/card_3.png b/demos/retro/card_3.png new file mode 100644 index 0000000..c270870 Binary files /dev/null and b/demos/retro/card_3.png differ diff --git a/demos/retro/card_4.png b/demos/retro/card_4.png new file mode 100644 index 0000000..75353fd Binary files /dev/null and b/demos/retro/card_4.png differ diff --git a/demos/retro/card_5.png b/demos/retro/card_5.png new file mode 100644 index 0000000..263f9b9 Binary files /dev/null and b/demos/retro/card_5.png differ diff --git a/demos/retro/cover.png b/demos/retro/cover.png new file mode 100644 index 0000000..1222e08 Binary files /dev/null and b/demos/retro/cover.png differ diff --git a/demos/terminal/card_1.png b/demos/terminal/card_1.png new file mode 100644 index 0000000..475ac84 Binary files /dev/null and b/demos/terminal/card_1.png differ diff --git a/demos/terminal/card_2.png b/demos/terminal/card_2.png new file mode 100644 index 0000000..875f499 Binary files /dev/null and b/demos/terminal/card_2.png differ diff --git a/demos/terminal/card_3.png b/demos/terminal/card_3.png new file mode 100644 index 0000000..9e7df24 Binary files /dev/null and b/demos/terminal/card_3.png differ diff --git a/demos/terminal/card_4.png b/demos/terminal/card_4.png new file mode 100644 index 0000000..761d6fb Binary files /dev/null and b/demos/terminal/card_4.png differ diff --git a/demos/terminal/card_5.png b/demos/terminal/card_5.png new file mode 100644 index 0000000..3f3a5a7 Binary files /dev/null and b/demos/terminal/card_5.png differ diff --git a/demos/terminal/cover.png b/demos/terminal/cover.png new file mode 100644 index 0000000..a4de2d8 Binary files /dev/null and b/demos/terminal/cover.png differ diff --git a/package.json b/package.json index 95d0026..079c7de 100644 --- a/package.json +++ b/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" + } } diff --git a/requirements.txt b/requirements.txt index 4243c12..c161fad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/publish_xhs.py b/scripts/publish_xhs.py index 28a6b96..0271365 100644 --- a/scripts/publish_xhs.py +++ b/scripts/publish_xhs.py @@ -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__': diff --git a/scripts/render_xhs.js b/scripts/render_xhs.js index 6d08120..314c262 100644 --- a/scripts/render_xhs.js +++ b/scripts/render_xhs.js @@ -1,188 +1,511 @@ #!/usr/bin/env node /** - * 小红书卡片渲染脚本 - Node.js 版本 - * 将 Markdown 文件渲染为小红书风格的图片卡片 + * 小红书卡片渲染脚本 - Node.js 增强版 + * 支持多种排版样式和智能分页策略 * * 使用方法: - * node render_xhs.js [--output-dir ] + * node render_xhs.js [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 [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 = '
'; - for (const tag of tags) { - tagsHtml += `${tag}`; - } - tagsHtml += '
'; - } +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 ` + + + + + 小红书封面 + + + +
+
+
${emoji}
+
${title}
+
${subtitle}
+
+
+ +`; } /** * 生成正文卡片 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 ` + + + + + 小红书卡片 + + + +
+
+
+
+ ${htmlContent} +
+
+
+
${pageText}
+
+ +`; } /** - * 使用 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 [--output-dir ]'); - 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); diff --git a/scripts/render_xhs.py b/scripts/render_xhs.py index 38f6d39..952bd0f 100644 --- a/scripts/render_xhs.py +++ b/scripts/render_xhs.py @@ -1,13 +1,27 @@ #!/usr/bin/env python3 """ -小红书卡片渲染脚本 - Python 版本 -将 Markdown 文件渲染为小红书风格的图片卡片 +小红书卡片渲染脚本 - 增强版 +支持多种排版样式和智能分页策略 使用方法: - python render_xhs.py [--output-dir ] + python render_xhs.py [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''' + + + + + 小红书封面 + + + +
+
+
{emoji}
+
{title}
+
{subtitle}
+
+
+ +''' 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''' + + + + + 小红书卡片 + + + +
+
+
+
{html_content}
+
+
+
{page_text}
+
+ +''' 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__':