Skill: hhua007/rednote-publish-auto (version 1.0.1)

--- name: xhs-publisher description: 小红书笔记创作与发布完整技能。用户给定主题或资料，自动完成：文案撰写 → Markdown 卡片生成 → 图片渲染 → 小红书发布，全流程一键完成。 author: merged from content-writer + xhs-note-creator license: MIT platforms: - openclaw tags: - xhs - 小红书 - content - social-media - publishing --- # 小红书笔记创作与发布技能 一键完成从主题到小红书发布的全流程：文案撰写 → 生成 Markdown → 渲染图片卡片 → 自动发布。 ## 使用方式 当用户说「发小红书」「写一篇小红书」「发布到小红书」「生成小红书笔记」时触发本技能。 ### 输入确认 收到请求后，开始制作。第四步「发布小红书」前会展示内容预览， **必须等用户明确确认「可以发布」后才执行发布**，不会自动发布。 ### 工作流程 自动完成以下 4 步，第四步需用户确认： #### 第一步：撰写小红书文案 根据用户主题，生成完整小红书风格内容，包括： - 吸引眼球的标题（≤20字） - 钩子开头（1-2句激发好奇） - 正文（分段清晰，每段 2-3 句，含 emoji 点缀） - 总结 + CTA - Tags 标签（5-10 个） #### 第二步：生成渲染用 Markdown 将文案写成专用于图片渲染的 Markdown 文件： ```markdown --- emoji: "🤖" title: "大标题（≤15字）" subtitle: "副标题（≤15字）" --- # 第一部分标题 正文内容... # 第二部分标题 正文内容... ... ``` **注意**：正文用 `---` 分隔符分成多个段落，每段约 200-400 字左右，避免单段内容过长导致渲染被截断。 #### 第三步：渲染图片卡片 ```bash python3 scripts/render_xhs.py <markdown_file> -o <输出目录> -m auto-split -t default ``` - 推荐模式：`auto-split`（根据内容高度自动切分，不会截断内容） - 默认主题：`default`（简约灰白） - 生成的图片：封面 `cover.png` + 正文卡片 `card_1.png` `card_2.png` ... #### 第四步：发布小红书 ```bash XHS_COOKIE=<cookie> python3 scripts/publish_xhs.py \ --title "<标题>" \ --desc "<描述>" \ --images cover.png card_1.png card_2.png ... ``` - Cookie 从 `~/.openclaw/workspace/.xhs_cookie.env` 读取 - Cookie 失效时告知用户重新提供 --- ## 图片规格 | 类型 | 尺寸 | 比例 | |------|------|------| | 封面 | 1080×1440px | 3:4 | | 正文卡片 | 1080×? px | 3:4（高度按内容自适应）| --- ## 可用主题 | 主题 | 风格 | |------|------| | `default` | 简约灰白（默认）| | `playful-geometric` | 活泼几何（紫粉渐变）| | `neo-brutalism` | 新粗野主义（红黄撞色）| | `botanical` | 植物园自然（绿色系）| | `professional` | 专业商务（蓝色系）| | `retro` | 复古怀旧（橙棕系）| | `terminal` | 终端命令行（暗黑系）| | `sketch` | 手绘素描（灰调）| --- ## 分页模式说明 | 模式 | 适用场景 | |------|---------| | `auto-split` | **推荐**。内容长短不稳定时使用，自动切分不会截断 | | `separator` | 内容已手动用 `---` 控制好量 | | `auto-fit` | 固定尺寸，整体缩放文字填满 | | `dynamic` | 根据内容动态调整高度（仅适合短内容）| --- ## Cookie 配置 > ⚠️ **安全警告**：Cookie 等同于账号密码！ > - 持有此 Cookie 可代表您的账号进行发布操作 > - 请勿在公开场合分享 > - 建议使用专用小号，并及时在网页端撤销授权 Cookie 保存在：`~/.openclaw/workspace/.xhs_cookie.env` 格式： ``` XHS_COOKIE=your_cookie_string_here ``` 获取方式： 1. 浏览器登录小红书（https://www.xiaohongshu.com） 2. F12 → Network → 任意请求 → Request Headers → 复制 `Cookie` 字段 3. 告知 Agent 更新 4. 如需撤销：网页端 → 设置 → 账号安全 → 退出登录对应设备 --- ## 依赖安装 如遇缺少依赖提示，运行： ```bash pip install markdown PyYAML playwright xhs python-dotenv requests playwright install chromium ```

path: requirements.txt
content: # 小红书笔记创作技能依赖

# Markdown 处理
markdown>=3.4.0
PyYAML>=6.0

# 浏览器自动化（渲染图片）
playwright>=1.40.0

# 小红书发布
xhs>=0.4.0

# 环境变量管理
python-dotenv>=1.0.0

# HTTP 请求（API 模式）
requests>=2.28.0


path: assets/cover.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=1080, height=1440">
    <title>小红书封面</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
        
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            width: 1080px;
            height: 1440px;
            overflow: hidden;
        }
        
        .cover-container {
            width: 1080px;
            height: 1440px;
            background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
            position: relative;
            overflow: hidden;
        }
        
        .cover-inner {
            position: absolute;
            width: 950px;
            height: 1310px;
            left: 65px;
            top: 65px;
            background: #F3F3F3;
            border-radius: 25px;
            display: flex;
            flex-direction: column;
            padding: 80px 85px;
        }
        
        .cover-emoji {
            font-size: 180px;
            line-height: 1.2;
            margin-bottom: 50px;
        }
        
        .cover-title {
            font-weight: 900;
            font-size: 130px;
            line-height: 1.4;
            background: linear-gradient(180deg, #111827 0%, #4B5563 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            flex: 1;
            display: flex;
            align-items: flex-start;
            word-break: normal;
            overflow-wrap: break-word;
        }
        
        .cover-subtitle {
            font-weight: 350;
            font-size: 72px;
            line-height: 1.4;
            color: #000000;
            margin-top: auto;
        }
    </style>
</head>
<body>
    <div class="cover-container">
        <div class="cover-inner">
            <div class="cover-emoji">{{EMOJI}}</div>
            <div class="cover-title">{{TITLE}}</div>
            <div class="cover-subtitle">{{SUBTITLE}}</div>
        </div>
    </div>
</body>
</html>


path: assets/example.md
content: ---
emoji: "🎨"
title: "8种超美排版风格"
subtitle: "小红书笔记创作神器"
---

# 活泼几何风格 💜

这是 **Playful Geometric** 风格的示例，灵感来自 Memphis 设计。

> 糖果般的色彩搭配，硬边框与圆角的对比，让内容更有活力！

## 特点

- 紫色、粉色、黄色的俏皮配色
- 硬阴影贴纸效果
- 非对称圆角设计

---

# 新粗野主义风格 ⚡

**Neo-Brutalism** 风格：RAW. LOUD. UNAPOLOGETIC.

> 厚重的黑色边框，高饱和度色块，这就是反设计美学！

## 视觉特点

- 5px 以上的粗边框
- 8-10px 的硬阴影
- 荧光黄与电光青的撞色

---

# 植物园风格 🌿

**Botanical** 风格带来自然清新的感觉。

> 森林绿与米白的搭配，仿佛置身于植物园中

## 设计理念

- 柔和的绿色系配色
- 自然有机的圆角
- 温暖的米色背景

---

# 更多风格选择

还有更多精美风格等你探索：

1. **Professional** - 专业商务风格
2. **Retro** - 复古怀旧风格
3. **Terminal** - 终端命令行风格
4. **Sketch** - 手绘素描风格

使用命令：
```bash
python render_xhs.py example.md -t playful-geometric
```

#小红书模板 #排版设计 #内容创作


path: assets/styles.css
content: /* 小红书卡片样式 */

/* CSS 变量定义 */
:root {
    --primary-color: #ff2442;
    --primary-light: #ff6b81;
    --text-primary: #1e293b;
    --text-secondary: #334155;
    --text-muted: #64748b;
    --text-light: #475569;
    --background-light: #f8fafc;
    --background-gray: #f1f5f9;
    --border-color: #e2e8f0;
    --border-radius: 25px;
    --card-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
    --font-family: 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', -apple-system, sans-serif;
}

/* 全局样式重置 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: var(--font-family);
    background: transparent;
    color: var(--text-secondary);
    line-height: 1.6;
}

/* ========================
   封面卡片样式
   ======================== */

.cover-container {
    width: 1080px;
    height: 1440px;
    background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
    position: relative;
    overflow: hidden;
}

.cover-inner {
    position: absolute;
    width: 950px;
    height: 1310px;
    left: 65px;
    top: 65px;
    background: #F3F3F3;
    border-radius: var(--border-radius);
    display: flex;
    flex-direction: column;
    padding: 60px 85px;
}

.cover-emoji {
    font-size: 180px;
    line-height: 1.2;
    margin-bottom: 40px;
}

.cover-title {
    font-weight: 900;
    font-size: 130px;
    line-height: 1.35;
    background: linear-gradient(180deg, #111827 0%, #4B5563 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    margin-bottom: auto;
    word-break: normal;
    overflow-wrap: break-word;
}

.cover-subtitle {
    font-weight: 350;
    font-size: 72px;
    line-height: 1.4;
    color: #000000;
    margin-top: auto;
    padding-bottom: 20px;
}

/* ========================
   正文卡片样式
   ======================== */

.card-container {
    width: 1080px;
    min-height: 1440px;
    background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
    position: relative;
    padding: 50px;
    overflow: hidden;
}

.card-inner {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 20px;
    padding: 60px;
    min-height: calc(1440px - 100px);
    box-shadow: var(--card-shadow);
    backdrop-filter: blur(10px);
}

/* Markdown 内容样式 */
.card-content {
    color: var(--text-light);
    font-size: 42px;
    line-height: 1.7;
}

.card-content h1 {
    font-size: 72px;
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 40px;
    line-height: 1.3;
}

.card-content h2 {
    font-size: 56px;
    font-weight: 600;
    color: var(--text-secondary);
    margin: 50px 0 25px 0;
    line-height: 1.4;
}

.card-content h3 {
    font-size: 48px;
    font-weight: 600;
    color: var(--text-light);
    margin: 40px 0 20px 0;
}

.card-content p {
    margin-bottom: 35px;
}

.card-content strong {
    font-weight: 700;
    color: var(--text-primary);
}

.card-content em {
    font-style: italic;
    color: var(--primary-color);
}

.card-content a {
    color: var(--primary-color);
    text-decoration: none;
    border-bottom: 2px solid var(--primary-color);
}

.card-content ul,
.card-content ol {
    margin: 30px 0;
    padding-left: 60px;
}

.card-content li {
    margin-bottom: 20px;
    line-height: 1.6;
}

.card-content blockquote {
    border-left: 8px solid var(--primary-color);
    padding-left: 40px;
    background: var(--background-gray);
    padding-top: 25px;
    padding-bottom: 25px;
    margin: 35px 0;
    color: var(--text-muted);
    font-style: italic;
    border-radius: 0 12px 12px 0;
}

.card-content blockquote p {
    margin: 0;
}

.card-content code {
    background: var(--background-gray);
    padding: 6px 16px;
    border-radius: 8px;
    font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
    font-size: 38px;
    color: var(--primary-color);
}

.card-content pre {
    background: var(--text-primary);
    color: #e2e8f0;
    padding: 40px;
    border-radius: 16px;
    margin: 35px 0;
    overflow-x: visible;
    overflow-wrap: break-word;
    word-wrap: break-word;
    overflow-wrap: break-word;
    word-wrap: break-word;
    word-break: normal;
    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: var(--border-color);
    margin: 50px 0;
}

/* Tags 标签样式 */
.card-content .tags {
    margin-top: 50px;
    padding-top: 30px;
    border-top: 2px solid var(--border-color);
}

.card-content .tag {
    display: inline-block;
    background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
    color: white;
    padding: 12px 28px;
    border-radius: 30px;
    font-size: 34px;
    margin: 10px 15px 10px 0;
    font-weight: 500;
}

/* 信息卡片样式 */
.info-card {
    margin: 40px 0;
    padding: 40px 50px;
    background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
    border: 2px solid rgba(99, 102, 241, 0.2);
    border-radius: 20px;
    box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1);
}

.info-card.success {
    background: linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%);
    border-color: rgba(16, 185, 129, 0.2);
}

.info-card.warning {
    background: linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(217, 119, 6, 0.12) 100%);
    border-color: rgba(245, 158, 11, 0.2);
}

.info-card.error {
    background: linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.12) 100%);
    border-color: rgba(239, 68, 68, 0.2);
}

/* 页码样式 */
.page-number {
    position: absolute;
    bottom: 80px;
    right: 80px;
    font-size: 36px;
    color: rgba(255, 255, 255, 0.8);
    font-weight: 500;
}

/* ========================
   渐变背景预设
   ======================== */

.bg-gradient-1 {
    background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
}

.bg-gradient-2 {
    background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
}

.bg-gradient-3 {
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}

.bg-gradient-4 {
    background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}

.bg-gradient-5 {
    background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}

.bg-gradient-6 {
    background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}

.bg-gradient-7 {
    background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
}

.bg-gradient-8 {
    background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}

path: assets/card.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=1080">
    <title>小红书卡片</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
        
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            width: 1080px;
            min-height: 1440px;
            overflow: hidden;
            background: transparent;
        }
        
        .card-container {
            width: 1080px;
            min-height: 1440px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            position: relative;
            padding: 50px;
            overflow: hidden;
        }
        
        .card-inner {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            min-height: calc(1440px - 100px);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
        }
        
        /* Markdown 内容样式 */
        .card-content {
            color: #475569;
            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 {
            font-weight: 700;
            color: #1e293b;
        }
        
        .card-content em {
            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;
        }
        
        /* 信息卡片样式 */
        .info-card {
            margin: 40px 0;
            padding: 40px 50px;
            background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
            border: 2px solid rgba(99, 102, 241, 0.2);
            border-radius: 20px;
            box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1);
        }
        
        /* 页码样式 */
        .page-number {
            position: absolute;
            bottom: 80px;
            right: 80px;
            font-size: 36px;
            color: rgba(255, 255, 255, 0.8);
            font-weight: 500;
        }
    </style>
</head>
<body>
    <div class="card-container">
        <div class="card-inner">
            <div class="card-content">
                {{CONTENT}}
            </div>
        </div>
        <div class="page-number">{{PAGE_NUMBER}}</div>
    </div>
</body>
</html>


path: assets/themes/neo-brutalism.css
content: /* ============================================
   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;
}


path: assets/themes/terminal.css
content: /* ============================================
   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;
}


path: assets/themes/botanical.css
content: /* ============================================
   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;
}


path: assets/themes/playful-geometric.css
content: /* ============================================
   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;
}


path: assets/themes/retro.css
content: /* ============================================
   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;
}


path: assets/themes/professional.css
content: /* ============================================
   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;
}


path: assets/themes/default.css
content: /* ============================================
   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;
}


path: assets/themes/sketch.css
content: /* ============================================
   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);
}


path: scripts/publish_xhs.py
content: #!/usr/bin/env python3
"""
小红书笔记发布脚本 - 增强版
支持直接发布（本地签名）和通过 API 服务发布两种方式

使用方法:
    # 直接发布（使用本地签名）
    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 文件，配置：
    
    # 必需：小红书 Cookie
    XHS_COOKIE=your_cookie_string_here
    
    # 可选：API 服务地址（使用 --api-mode 时需要）
    XHS_API_URL=http://localhost:5005

依赖安装:
    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
    import requests
except ImportError as e:
    print(f"缺少依赖: {e}")
    print("请运行: pip install python-dotenv requests")
    sys.exit(1)


def load_cookie() -> str:
    """从 .env 文件加载 Cookie"""
    # 尝试从多个位置加载 .env
    env_paths = [
        Path.cwd() / '.env',
        Path(__file__).parent.parent / '.env',
        Path(__file__).parent.parent.parent / '.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("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 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_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:
        if os.path.exists(path):
            valid_images.append(os.path.abspath(path))
        else:
            print(f"⚠️ 警告: 图片不存在 - {path}")
    
    if not valid_images:
        print("❌ 错误: 没有有效的图片文件")
        sys.exit(1)
    
    return valid_images


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="", x_s="", x_t="", b1="", web_session=""):
            # XhsClient 内部调用时传的是 a1 参数名
            return local_sign(uri, data, a1=a1)
        
        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)}")
        
        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


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='将图片发布为小红书笔记',
        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',
        required=True,
        help='笔记标题（不超过20字）'
    )
    parser.add_argument(
        '--desc', '-d',
        default='',
        help='笔记描述/正文内容'
    )
    parser.add_argument(
        '--images', '-i',
        nargs='+',
        required=True,
        help='图片文件路径（可以多个）'
    )
    parser.add_argument(
        '--private',
        action='store_true',
        help='是否设为私密笔记'
    )
    parser.add_argument(
        '--post-time',
        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',
        help='仅验证，不实际发布'
    )
    
    args = parser.parse_args()
    
    # 验证标题长度
    if len(args.title) > 20:
        print(f"⚠️ 警告: 标题超过20字，将被截断")
        args.title = args.title[:20]
    
    # 加载 Cookie
    cookie = load_cookie()
    
    # 验证 Cookie 格式
    validate_cookie(cookie)
    
    # 验证图片
    valid_images = validate_images(args.images)
    
    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()
    
    # 发布笔记
    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__':
    main()


path: scripts/render_xhs.js
content: #!/usr/bin/env node
/**
 * 小红书卡片渲染脚本 - Node.js 增强版
 * 支持多种排版样式和智能分页策略
 * 
 * 使用方法:
 *     node render_xhs.js <markdown_file> [options]
 * 
 * 选项:
 *     --output-dir, -o     输出目录（默认为当前工作目录）
 *     --theme, -t          排版主题：default, playful-geometric, neo-brutalism, 等
 *     --mode, -m           分页模式：separator, auto-fit, auto-split, dynamic
 *     --width, -w          图片宽度（默认 1080）
 *     --height, -h         图片高度（默认 1440）
 *     --dpr                设备像素比（默认 2）
 * 
 * 依赖安装:
 *     npm install marked yaml playwright
 *     npx playwright install chromium
 */

const fs = require('fs');
const path = require('path');
const { marked } = require('marked');
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 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%)',
};

/**
 * 解析命令行参数
 */
function parseArgs() {
    const args = process.argv.slice(2);
    const options = {
        markdownFile: null,
        outputDir: process.cwd(),
        theme: 'default',
        mode: 'separator',
        width: DEFAULT_WIDTH,
        height: DEFAULT_HEIGHT,
        maxHeight: MAX_HEIGHT,
        dpr: 2
    };

    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        const nextArg = args[i + 1];

        switch (arg) {
            case '--output-dir':
            case '-o':
                options.outputDir = nextArg;
                i++;
                break;
            case '--theme':
            case '-t':
                options.theme = nextArg;
                i++;
                break;
            case '--mode':
            case '-m':
                options.mode = nextArg;
                i++;
                break;
            case '--width':
            case '-w':
                options.width = parseInt(nextArg);
                i++;
                break;
            case '--height':
                options.height = parseInt(nextArg);
                i++;
                break;
            case '--max-height':
                options.maxHeight = parseInt(nextArg);
                i++;
                break;
            case '--dpr':
                options.dpr = parseInt(nextArg);
                i++;
                break;
            case '--help':
                printHelp();
                process.exit(0);
            default:
                if (!arg.startsWith('-')) {
                    options.markdownFile = arg;
                }
        }
    }

    return options;
}

/**
 * 打印帮助信息
 */
function printHelp() {
    console.log(`
小红书卡片渲染脚本 - Node.js 版本

使用方法:
    node render_xhs.js <markdown_file> [options]

选项:
    --output-dir, -o     输出目录（默认为当前工作目录）
    --theme, -t          排版主题
    --mode, -m           分页模式
    --width, -w          图片宽度（默认 1080）
    --height             图片高度（默认 1440）
    --max-height         最大高度（默认 2160）
    --dpr                设备像素比（默认 2）

可用主题: ${AVAILABLE_THEMES.join(', ')}
分页模式: ${PAGING_MODES.join(', ')}
`);
}

/**
 * 解析 Markdown 文件
 */
function parseMarkdownFile(filePath) {
    const content = fs.readFileSync(filePath, 'utf-8');
    
    // 解析 YAML 头部
    const yamlMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
    
    let metadata = {};
    let body = content;
    
    if (yamlMatch) {
        try {
            metadata = yaml.parse(yamlMatch[1]) || {};
        } catch (e) {
            metadata = {};
        }
        body = content.slice(yamlMatch[0].length);
    }
    
    return { metadata, body: body.trim() };
}

/**
 * 按分隔符拆分内容
 */
function splitContentBySeparator(body) {
    const parts = body.split(/\n---+\n/);
    return parts.map(p => p.trim()).filter(p => p);
}

/**
 * 加载主题 CSS
 */
function loadThemeCss(theme) {
    const themeFile = path.join(THEMES_DIR, `${theme}.css`);
    if (fs.existsSync(themeFile)) {
        return fs.readFileSync(themeFile, '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, 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);
    
    const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
    const titleBg = THEME_TITLE_GRADIENTS[theme] || THEME_TITLE_GRADIENTS['default'];
    
    return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=${width}, height=${height}">
    <title>小红书封面</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
        
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            width: ${width}px;
            height: ${height}px;
            overflow: hidden;
        }
        
        .cover-container {
            width: ${width}px;
            height: ${height}px;
            background: ${bg};
            position: relative;
            overflow: hidden;
        }
        
        .cover-inner {
            position: absolute;
            width: ${Math.floor(width * 0.88)}px;
            height: ${Math.floor(height * 0.91)}px;
            left: ${Math.floor(width * 0.06)}px;
            top: ${Math.floor(height * 0.045)}px;
            background: #F3F3F3;
            border-radius: 25px;
            display: flex;
            flex-direction: column;
            padding: ${Math.floor(width * 0.074)}px ${Math.floor(width * 0.079)}px;
        }
        
        .cover-emoji {
            font-size: ${Math.floor(width * 0.167)}px;
            line-height: 1.2;
            margin-bottom: ${Math.floor(height * 0.035)}px;
        }
        
        .cover-title {
            font-weight: 900;
            font-size: ${Math.floor(width * 0.12)}px;
            line-height: 1.4;
            background: ${titleBg};
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            flex: 1;
            display: flex;
            align-items: flex-start;
            word-break: break-all;
        }
        
        .cover-subtitle {
            font-weight: 350;
            font-size: ${Math.floor(width * 0.067)}px;
            line-height: 1.4;
            color: #000000;
            margin-top: auto;
        }
    </style>
</head>
<body>
    <div class="cover-container">
        <div class="cover-inner">
            <div class="cover-emoji">${emoji}</div>
            <div class="cover-title">${title}</div>
            <div class="cover-subtitle">${subtitle}</div>
        </div>
    </div>
</body>
</html>`;
}

/**
 * 生成正文卡片 HTML
 */
function generateCardHtml(content, 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'];
    
    let containerStyle, innerStyle, contentStyle;
    
    if (mode === 'auto-fit') {
        containerStyle = `
            width: ${width}px;
            height: ${height}px;
            background: ${bg};
            position: relative;
            padding: 50px;
            overflow: hidden;
        `;
        innerStyle = `
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            height: calc(${height}px - 100px);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
            overflow: hidden;
            display: flex;
            flex-direction: column;
        `;
        contentStyle = 'flex: 1; overflow: hidden;';
    } else if (mode === 'dynamic') {
        containerStyle = `
            width: ${width}px;
            min-height: ${height}px;
            background: ${bg};
            position: relative;
            padding: 50px;
        `;
        innerStyle = `
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
        `;
        contentStyle = '';
    } else {
        containerStyle = `
            width: ${width}px;
            min-height: ${height}px;
            background: ${bg};
            position: relative;
            padding: 50px;
            overflow: hidden;
        `;
        innerStyle = `
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            min-height: calc(${height}px - 100px);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
        `;
        contentStyle = '';
    }
    
    return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=${width}">
    <title>小红书卡片</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
        
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            width: ${width}px;
            overflow: hidden;
            background: transparent;
        }
        
        .card-container { ${containerStyle} }
        .card-inner { ${innerStyle} }
        .card-content { line-height: 1.7; ${contentStyle} }
        /* auto-fit 用：对整个内容块做 transform 缩放 */
        .card-content-scale { transform-origin: top left; will-change: transform; }
        
        ${themeCss}
        
        .page-number {
            position: absolute;
            bottom: 80px;
            right: 80px;
            font-size: 36px;
            color: rgba(255, 255, 255, 0.8);
            font-weight: 500;
        }
    </style>
</head>
<body>
    <div class="card-container">
        <div class="card-inner">
            <div class="card-content">
                <div class="card-content-scale">
                    ${htmlContent}
                </div>
            </div>
        </div>
        <div class="page-number">${pageText}</div>
    </div>
</body>
</html>`;
}

/**
 * 渲染 HTML 为图片
 */
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: viewportHeight },
        deviceScaleFactor: dpr
    });
    
    await page.setContent(htmlContent);
    await page.waitForLoadState('networkidle');
    await page.waitForTimeout(500);
    
    let actualHeight;
    
    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'
    });
    
    await browser.close();
    console.log(`  ✅ 已生成: ${outputPath} (${width}x${actualHeight})`);
    return actualHeight;
}

/**
 * 主渲染函数
 */
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)) {
        fs.mkdirSync(outputDir, { recursive: true });
    }
    
    // 解析 Markdown 文件
    const { metadata, body } = parseMarkdownFile(markdownFile);
    
    // 分割内容
    const cardContents = splitContentBySeparator(body);
    const totalCards = cardContents.length;
    
    console.log(`  📄 检测到 ${totalCards} 张正文卡片`);
    
    // 生成封面
    if (metadata.emoji || metadata.title) {
        console.log('  📷 生成封面...');
        const coverHtml = generateCoverHtml(metadata, theme, width, height);
        const coverPath = path.join(outputDir, 'cover.png');
        await renderHtmlToImage(coverHtml, coverPath, width, height, 'separator', maxHeight, dpr);
    }
    
    // 生成正文卡片
    for (let i = 0; i < cardContents.length; i++) {
        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}`);
}

/**
 * 主函数
 */
async function main() {
    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(console.error);


path: scripts/render_xhs.py
content: #!/usr/bin/env python3
"""
小红书卡片渲染脚本 - 增强版
支持多种排版样式和智能分页策略

使用方法:
    python render_xhs.py <markdown_file> [options]

选项:
    --output-dir, -o     输出目录（默认为当前工作目录）
    --theme, -t          排版主题：default, playful-geometric, neo-brutalism, 
                         botanical, professional, retro, terminal, sketch
    --mode, -m           分页模式：
                         - separator  : 按 --- 分隔符手动分页（默认）
                         - auto-fit   : 自动缩放文字以填满固定尺寸
                         - auto-split : 根据内容高度自动切分
                         - dynamic    : 根据内容动态调整图片高度
    --width, -w          图片宽度（默认 1080）
    --height, -h         图片高度（默认 1440，dynamic 模式下为最小高度）
    --max-height         dynamic 模式下的最大高度（默认 4320
    --dpr                设备像素比（默认 2）

依赖安装:
    pip install markdown pyyaml playwright
    playwright install chromium
"""

import argparse
import asyncio
import os
import re
import sys
import tempfile
from pathlib import Path
from typing import List, Dict, Any, Optional

try:
    import markdown
    import yaml
    from playwright.async_api import async_playwright
except ImportError as e:
    print(f"缺少依赖: {e}")
    print("请运行: pip install markdown pyyaml playwright && playwright install chromium")
    sys.exit(1)


# 获取脚本所在目录
SCRIPT_DIR = Path(__file__).parent.parent
ASSETS_DIR = SCRIPT_DIR / "assets"
THEMES_DIR = ASSETS_DIR / "themes"

# 默认卡片尺寸配置 (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:
    """解析 Markdown 文件，提取 YAML 头部和正文内容"""
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 解析 YAML 头部
    yaml_pattern = r'^---\s*\n(.*?)\n---\s*\n'
    yaml_match = re.match(yaml_pattern, content, re.DOTALL)
    
    metadata = {}
    body = content
    
    if yaml_match:
        try:
            metadata = yaml.safe_load(yaml_match.group(1)) or {}
        except yaml.YAMLError:
            metadata = {}
        body = content[yaml_match.end():]
    
    return {
        'metadata': metadata,
        'body': body.strip()
    }


def split_content_by_separator(body: str) -> List[str]:
    """按照 --- 分隔符拆分正文为多张卡片内容"""
    parts = re.split(r'\n---+\n', body)
    return [part.strip() for part in parts if part.strip()]


def convert_markdown_to_html(md_content: str) -> str:
    """将 Markdown 转换为 HTML"""
    # 处理 tags（以 # 开头的标签）
    tags_pattern = r'((?:#[\w\u4e00-\u9fa5]+\s*)+)$'
    tags_match = re.search(tags_pattern, md_content, re.MULTILINE)
    tags_html = ""
    
    if tags_match:
        tags_str = tags_match.group(1)
        md_content = md_content[:tags_match.start()].strip()
        tags = re.findall(r'#([\w\u4e00-\u9fa5]+)', tags_str)
        if tags:
            tags_html = '<div class="tags-container">'
            for tag in tags:
                tags_html += f'<span class="tag">#{tag}</span>'
            tags_html += '</div>'
    
    # 转换 Markdown 为 HTML
    html = markdown.markdown(
        md_content,
        extensions=['extra', 'codehilite', 'tables', 'nl2br']
    )
    
    return html + tags_html


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, theme: str, width: int, height: int) -> str:
    """生成封面 HTML"""
    emoji = metadata.get('emoji', '📝')
    title = metadata.get('title', '标题')
    subtitle = metadata.get('subtitle', '')
    
    
    # 动态调整标题字体大小
    title_len = len(title)
    if title_len <= 6:
        title_size = int(width * 0.14)  # 极大
    elif title_len <= 10:
        title_size = int(width * 0.12)  # 大
    elif title_len <= 18:
        title_size = int(width * 0.09)  # 中
    elif title_len <= 30:
        title_size = int(width * 0.07)  # 小
    else:
        title_size = int(width * 0.055) # 极小

    # 获取主题背景色
    theme_backgrounds = {
        'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
        'playful-geometric': 'linear-gradient(180deg, #8B5CF6 0%, #F472B6 100%)',
        'neo-brutalism': 'linear-gradient(180deg, #FF4757 0%, #FECA57 100%)',
        'botanical': 'linear-gradient(180deg, #4A7C59 0%, #8FBC8F 100%)',
        'professional': 'linear-gradient(180deg, #2563EB 0%, #3B82F6 100%)',
        'retro': 'linear-gradient(180deg, #D35400 0%, #F39C12 100%)',
        'terminal': 'linear-gradient(180deg, #0D1117 0%, #21262D 100%)',
        'sketch': 'linear-gradient(180deg, #555555 0%, #999999 100%)'
    }
    bg = theme_backgrounds.get(theme, theme_backgrounds['default'])

    # 封面标题文字渐变随主题变化
    title_gradients = {
        'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',
        'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',
        'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',
        'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',
        'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',
        'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',
        'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',
        'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',
    }
    title_bg = title_gradients.get(theme, title_gradients['default'])
    
    html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width={width}, height={height}">
    <title>小红书封面</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
        
        * {{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}
        
        body {{
            font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            width: {width}px;
            height: {height}px;
            overflow: hidden;
        }}
        
        .cover-container {{
            width: {width}px;
            height: {height}px;
            background: {bg};
            position: relative;
            overflow: hidden;
        }}
        
        .cover-inner {{
            position: absolute;
            width: {int(width * 0.88)}px;
            height: {int(height * 0.91)}px;
            left: {int(width * 0.06)}px;
            top: {int(height * 0.045)}px;
            background: #F3F3F3;
            border-radius: 25px;
            display: flex;
            flex-direction: column;
            padding: {int(width * 0.074)}px {int(width * 0.079)}px;
        }}
        
        .cover-emoji {{
            font-size: {int(width * 0.167)}px;
            line-height: 1.2;
            margin-bottom: {int(height * 0.035)}px;
        }}
        
        .cover-title {{
            font-weight: 900;
            font-size: {title_size}px;
            line-height: 1.4;
            background: {title_bg};
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            flex: 1;
            display: flex;
            align-items: flex-start;
            word-break: break-all;
        }}
        
        .cover-subtitle {{
            font-weight: 350;
            font-size: {int(width * 0.067)}px;
            line-height: 1.4;
            color: #000000;
            margin-top: auto;
        }}
    </style>
</head>
<body>
    <div class="cover-container">
        <div class="cover-inner">
            <div class="cover-emoji">{emoji}</div>
            <div class="cover-title">{title}</div>
            <div class="cover-subtitle">{subtitle}</div>
        </div>
    </div>
</body>
</html>'''
    return html


def generate_card_html(content: str, theme: str, page_number: int = 1, 
                       total_pages: int = 1, width: int = DEFAULT_WIDTH, 
                       height: int = DEFAULT_HEIGHT, mode: str = 'separator') -> str:
    """生成正文卡片 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 ""
    
    # 获取主题背景色
    theme_backgrounds = {
        'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
        'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',
        'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',
        'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',
        'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',
        'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',
        'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',
        'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'
    }
    bg = theme_backgrounds.get(theme, theme_backgrounds['default'])
    
    # 根据模式设置不同的容器样式
    if mode == 'auto-fit':
        container_style = f'''
            width: {width}px;
            height: {height}px;
            background: {bg};
            position: relative;
            padding: 50px;
            overflow: hidden;
        '''
        inner_style = f'''
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            height: calc({height}px - 100px);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
            overflow: hidden;
            display: flex;
            flex-direction: column;
        '''
        content_style = '''
            flex: 1;
            overflow: hidden;
        '''
    elif mode == 'dynamic':
        container_style = f'''
            width: {width}px;
            min-height: {height}px;
            background: {bg};
            position: relative;
            padding: 50px;
        '''
        inner_style = '''
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
        '''
        content_style = ''
    else:  # separator 和 auto-split
        container_style = f'''
            width: {width}px;
            min-height: {height}px;
            background: {bg};
            position: relative;
            padding: 50px;
            overflow: hidden;
        '''
        inner_style = f'''
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 60px;
            min-height: calc({height}px - 100px);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
        '''
        content_style = ''
    
    html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width={width}">
    <title>小红书卡片</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
        
        * {{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}
        
        body {{
            font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            width: {width}px;
            overflow: hidden;
            background: transparent;
        }}
        
        .card-container {{
            {container_style}
        }}
        
        .card-inner {{
            {inner_style}
        }}
        
        .card-content {{
            line-height: 1.7;
            {content_style}
        }}

        /* auto-fit 用：对整个内容块做 transform 缩放 */
        .card-content-scale {{
            transform-origin: top left;
            will-change: transform;
        }}
        
        {theme_css}
        
        .page-number {{
            position: absolute;
            bottom: 80px;
            right: 80px;
            font-size: 36px;
            color: rgba(255, 255, 255, 0.8);
            font-weight: 500;
        }}
    </style>
</head>
<body>
    <div class="card-container">
        <div class="card-inner">
            <div class="card-content">
                <div class="card-content-scale">{html_content}</div>
            </div>
        </div>
        <div class="page-number">{page_text}</div>
    </div>
</body>
</html>'''
    return html


async def render_html_to_image(html_content: str, output_path: str, 
                               width: int = 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()
        
        # 设置视口大小
        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:
            f.write(html_content)
            temp_html_path = f.name
        
        try:
            await page.goto(f'file://{temp_html_path}')
            await page.wait_for_load_state('networkidle')
            
            # 等待字体加载
            await page.wait_for_timeout(500)
            
            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(
                path=output_path,
                clip={'x': 0, 'y': 0, 'width': width, 'height': actual_height},
                type='png'
            )
            
            print(f"  ✅ 已生成: {output_path} ({width}x{actual_height})")
            return actual_height
            
        finally:
            os.unlink(temp_html_path)
            await browser.close()


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)
    
    # 解析 Markdown 文件
    data = parse_markdown_file(md_file)
    metadata = data['metadata']
    body = data['body']
    
    # 根据模式处理内容分割
    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, theme, width, height)
        cover_path = os.path.join(output_dir, 'cover.png')
        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, 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, width, height, mode, max_height, dpr)
    
    print(f"\n✨ 渲染完成！图片已保存到: {output_dir}")
    return total_cards


def main():
    parser = argparse.ArgumentParser(
        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',
        help='Markdown 文件路径'
    )
    parser.add_argument(
        '--output-dir', '-o',
        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()
    
    if not os.path.exists(args.markdown_file):
        print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
        sys.exit(1)
    
    asyncio.run(render_markdown_to_cards(
        args.markdown_file,
        args.output_dir,
        theme=args.theme,
        mode=args.mode,
        width=args.width,
        height=args.height,
        max_height=args.max_height,
        dpr=args.dpr
    ))


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)