Skill: 0xcjl/cjl-plugin (version 0.1.0)

--- name: cjl-plugin description: | CJL Skills Collection — a personal Claude Code plugin with 17 production skills. Use when the user wants to: read papers, create content cards, design presentations, analyze relationships, improve writing, research travel, learn vocabulary, and more. user_invocable: true version: "1.0.0" --- # CJL Skills Collection This is a **plugin** containing 17 individual skills. Activate any skill directly: | Command | Description | |---------|-------------| | `/cjl-card` | Content → PNG visuals (infographics, posters) | | `/cjl-paper` | Academic paper analysis | | `/cjl-paper-flow` | Paper analysis + PNG card pipeline | | `/cjl-paper-river` | Paper genealogy / citation tracing | | `/cjl-plain` | Plain language rewriter | | `/cjl-rank` | Dimensional reduction analysis | | `/cjl-relationship` | Relationship analysis | | `/cjl-roundtable` | Multi-perspective roundtable discussion | | `/cjl-skill-map` | Visual overview of installed skills | | `/cjl-travel` | City travel research workflow | | `/cjl-word` | English word deep-dive | | `/cjl-word-flow` | Word analysis → infographic card | | `/cjl-writes` | Writing engine for idea development | | `/cjl-x-download` | X/Twitter media downloader | | `/cjl-learn` | Concept dissection and learning | | `/cjl-invest` | Investment research and analysis | | `/cjl-slides` | HTML presentations in 24 design styles | ## Installation ``` /install-plugin https://github.com/0xcjl/cjl-plugin ``` ## Design Philosophy - **Atomic**: one skill, one responsibility - **Observable**: clear input → output contract - **Self-contained**: no external state dependencies - **User-invocable**: trigger via `/cjl-{name}` or natural language ## Credits Adapted from [lijigang/ljg-skills](https://github.com/lijigang/ljg-skills), with `ljg-` → `cjl-` renaming and the addition of `cjl-slides`.

path: CLAUDE.md
content: # CJL Skills Collection — Development Guide

This is a personal Claude Code skills repository. Each skill is a self-contained directory installable to `~/.claude/skills/` to extend Claude Code's capabilities.

## Repository Structure

```
cjl-plugin/
├── .claude-plugin/
│   └── plugin.json            # Plugin manifest
├── skills/                    # Skill directories
│   ├── cjl-*/                # Each skill has its own directory
│   │   └── SKILL.md          # Skill definition
│   └── cjl-slides/           # Complex skill with subdirectories
│       ├── SKILL.md
│       └── scripts/
└── README.md                 # English (default)
    README_zh.md              # 简体中文
    README_ja.md              # 日本語
```

## Skill Format

Each `SKILL.md` follows this structure:

```yaml
---
name: skill-name
description: "What this skill does. Use when user says..."
user_invocable: true|false
version: "x.x.x"
---

# Skill content in markdown...
```

## Skill Naming Convention

- All skills prefixed with `cjl-`
- Workflow skills (skill chains) include `-flow` suffix (e.g., `cjl-paper-flow`)
- Commands: `/cjl-{name}`

## Adding a New Skill

1. Create `skills/cjl-{name}/SKILL.md`
2. Add frontmatter with `name`, `description`, `user_invocable`, `version`
3. Write the skill content
4. Update `README.md` skill table
5. Update `plugin.json` version if needed

## Version Policy

- Each skill maintains its own `version` in SKILL.md frontmatter
- Plugin version in `plugin.json` is incremented for releases

## Testing Changes

After modifying a skill:
1. Copy to `~/.claude/skills/`
2. Restart Claude Code to reload skills
3. Test via `/cjl-{name}` or natural language trigger

## Multi-Language README

When adding a new language:
1. Create `README_{lang}.md` with proper frontmatter
2. Add `languages:` block to all README variants
3. Add cross-links between all README files


path: README.md
content: ---
languages:
  - en
  - zh
  - ja
---

# CJL Skills Collection

A personal Claude Code plugin providing 17 production-ready skills for research, content creation, presentation design, and workflow automation.

**English** | [简体中文](./README_zh.md) | [日本語](./README_ja.md)

---

## Skills

| Skill | Command | Description |
|-------|---------|-------------|
| `cjl-card` | `/cjl-card` | Content → PNG visuals (long cards, infographics, posters) |
| `cjl-paper` | `/cjl-paper` | Academic paper analysis pipeline |
| `cjl-paper-flow` | `/cjl-paper-flow` | Paper workflow (paper analysis + PNG card) |
| `cjl-paper-river` | `/cjl-paper-river` | Academic paper genealogy / citation tracing |
| `cjl-plain` | `/cjl-plain` | Plain language rewriter |
| `cjl-rank` | `/cjl-rank` | Dimensional reduction analysis |
| `cjl-relationship` | `/cjl-relationship` | Relationship analysis |
| `cjl-roundtable` | `/cjl-roundtable` | Multi-perspective roundtable discussion |
| `cjl-skill-map` | `/cjl-skill-map` | Visual overview of all installed skills |
| `cjl-travel` | `/cjl-travel` | City travel research workflow |
| `cjl-word` | `/cjl-word` | English word deep-dive with etymology |
| `cjl-word-flow` | `/cjl-word-flow` | Word analysis → infographic card |
| `cjl-writes` | `/cjl-writes` | Writing engine for thinking through ideas |
| `cjl-x-download` | `/cjl-x-download` | X/Twitter media downloader |
| `cjl-learn` | `/cjl-learn` | Concept dissection and learning |
| `cjl-invest` | `/cjl-invest` | Investment research and analysis |
| `cjl-slides` | `/cjl-slides` | HTML presentations in 24 international design styles |

---

## Design Philosophy

Each skill follows these principles:

- **Atomic**: One skill, one responsibility
- **Observable**: Clear input → output contract
- **Self-contained**: No external state dependencies
- **User-invocable**: Triggered via `/skill-name` or natural language

---

## Usage

### Install via Plugin

If your Claude Code supports plugin installation from GitHub:

```
/install-plugin https://github.com/0xcjl/cjl-plugin
```

### Manual Install

```bash
# Clone the repository
git clone https://github.com/0xcjl/cjl-plugin ~/.claude/plugins/cjl-plugin

# Skills will be available under ~/.claude/plugins/cjl-plugin/skills/
```

### Activate a Skill

```
/cjl-paper
/cjl-slides
/cjl-card
```

---

## Dependencies

| Skill | Dependency |
|-------|-----------|
| `cjl-card` | Node.js + Playwright |

---

## Development

See [CLAUDE.md](./CLAUDE.md) for development guidelines.

---

## Credits

Skills in this collection are adapted from [lijigang/ljg-skills](https://github.com/lijigang/ljg-skills), with skill renaming (`ljg-` → `cjl-`) and the addition of `cjl-slides`.

---

## License

MIT


path: README_ja.md
content: ---
languages:
  - en
  - zh
  - ja
---

# CJL スキルコレクション

研究、コンテンツ制作、プレゼンテーション設計、ワークフロー自動化 위한 17 のプロダクション対応スキルを備えた个人用 Claude Code プラグイン。

**English** | [简体中文](./README_zh.md) | [日本語](./README_ja.md)

---

## スキル一覧

| スキル | コマンド | 説明 |
|--------|----------|------|
| `cjl-card` | `/cjl-card` | コンテンツ → PNG ビジュアル（ロングカード、情報グラフィック、ポスター） |
| `cjl-paper` | `/cjl-paper` | 学術論文分析パイプライン |
| `cjl-paper-flow` | `/cjl-paper-flow` | 論文分析 + PNG カード ワークフロー |
| `cjl-paper-river` | `/cjl-paper-river` | 学術論文の系譜図 / 引用追跡 |
| `cjl-plain` | `/cjl-plain` | プレーンバゲージライター |
| `cjl-rank` | `/cjl-rank` | 次元削減分析 |
| `cjl-relationship` | `/cjl-relationship` | 関係性分析 |
| `cjl-roundtable` | `/cjl-roundtable` | マルチパースペクティブ円卓討議 |
| `cjl-skill-map` | `/cjl-skill-map` | インストール済みスキル可視化マップ |
| `cjl-travel` | `/cjl-travel` | 都市旅行研究ワークフロー |
| `cjl-word` | `/cjl-word` | 語源を含む英単語深掘り |
| `cjl-word-flow` | `/cjl-word-flow` | 単語分析 → 情報graphicカード |
| `cjl-writes` | `/cjl-writes` | アイデア整理のためのライティングエンジン |
| `cjl-x-download` | `/cjl-x-download` | X/Twitter メディアダウンローダー |
| `cjl-learn` | `/cjl-learn` | コンセプト解剖と学習 |
| `cjl-invest` | `/cjl-invest` | 投資調査と分析 |
| `cjl-slides` | `/cjl-slides` | 24 種類の国際デザインスタイルによる HTML プレゼンテーション |

---

## デザインの理念

各スキルは以下の原則に従います：

- **アトミック**: 1 スキル、1 責任
- **観察可能**: 明確な入力 → 出力契約
- **自己完結型**: 外部状態依存なし
- **ユーザー起動可能**: `/スキル名` または自然言語で起動

---

## 使用方法

### プラグインインストール

```bash
/install-plugin https://github.com/0xcjl/cjl-plugin
```

### 手動インストール

```bash
git clone https://github.com/0xcjl/cjl-plugin ~/.claude/plugins/cjl-plugin
```

---

## 依存関係

| スキル | 依存関係 | インストール |
|--------|----------|-------------|
| `cjl-card` | Node.js + Playwright | [cjl-card docs](https://github.com/0xcjl/cjl-plugin/tree/main/skills/cjl-card) を参照 |

---

## クレジット

[lijigang/ljg-skills](https://github.com/lijigang/ljg-skills) から改编。スキル名の変更（`ljg-` → `cjl-`）を実施し、`cjl-slides` を新規追加。

---

## ライセンス

MIT


path: README_zh.md
content: ---
languages:
  - en
  - zh
  - ja
---

# CJL 技能集合

个人 Claude Code 插件，提供 17 个可用于生产环境的技能，涵盖研究、内容创作、演示文稿设计和工作流自动化。

**English** | [简体中文](./README_zh.md) | [日本語](./README_ja.md)

---

## 技能一览

| 技能 | 命令 | 说明 |
|------|------|------|
| `cjl-card` | `/cjl-card` | 内容 → PNG 可视化（长图、信息图、海报） |
| `cjl-paper` | `/cjl-paper` | 学术论文分析流程 |
| `cjl-paper-flow` | `/cjl-paper-flow` | 论文分析 + PNG 卡片组合工作流 |
| `cjl-paper-river` | `/cjl-paper-river` | 学术论文溯源 / 引用关系追踪 |
| `cjl-plain` | `/cjl-plain` | 简明语言重写器 |
| `cjl-rank` | `/cjl-rank` | 降维分析 |
| `cjl-relationship` | `/cjl-relationship` | 关系分析 |
| `cjl-roundtable` | `/cjl-roundtable` | 多视角圆桌讨论 |
| `cjl-skill-map` | `/cjl-skill-map` | 已安装技能可视化总览 |
| `cjl-travel` | `/cjl-travel` | 城市旅行研究工作流 |
| `cjl-word` | `/cjl-word` | 英语单词深度学习（含词源） |
| `cjl-word-flow` | `/cjl-word-flow` | 单词分析 → 信息图卡片 |
| `cjl-writes` | `/cjl-writes` | 写作引擎，梳理思路 |
| `cjl-x-download` | `/cjl-x-download` | X/Twitter 媒体下载器 |
| `cjl-learn` | `/cjl-learn` | 概念拆解与学习 |
| `cjl-invest` | `/cjl-invest` | 投资研究与分析 |
| `cjl-slides` | `/cjl-slides` | HTML 演示文稿，24 种国际设计风格 |

---

## 设计理念

每个技能遵循以下原则：

- **原子化**：一个技能，一个职责
- **可观测**：输入 → 输出合约清晰
- **自包含**：无外部状态依赖
- **用户可触发**：通过 `/技能名` 或自然语言激活

---

## 使用方法

### 插件方式安装

```bash
/install-plugin https://github.com/0xcjl/cjl-plugin
```

### 手动安装

```bash
git clone https://github.com/0xcjl/cjl-plugin ~/.claude/plugins/cjl-plugin
```

### 激活技能

```
/cjl-paper
/cjl-slides
/cjl-card
```

---

## 依赖

| 技能 | 依赖 | 安装方式 |
|------|------|---------|
| `cjl-card` | Node.js + Playwright | 参见 [cjl-card 文档](https://github.com/0xcjl/cjl-plugin/tree/main/skills/cjl-card) |

---

## 开发指南

详见 [CLAUDE.md](./CLAUDE_zh.md)。

---

## 致谢

本集合技能改编自 [lijigang/ljg-skills](https://github.com/lijigang/ljg-skills)，已完成技能重命名（`ljg-` → `cjl-`），并新增 `cjl-slides` 技能。

---

## 许可证

MIT


path: skills/cjl-slides/STYLE_PREVIEWS.md
content: # 24 种设计风格预览

每种风格附：适用场景、一句话描述、标志性视觉特征。

---

## 1. Pitch.com — 商务优雅
**场景：** 融资路演、商务提案
**描述：** 大量留白、精致渐变、商务优雅感
**视觉特征：** 纯白底、超宽标题、精致的紫色渐变按钮

## 2. Bloomberg Businessweek — 新闻编辑实验风
**场景：** 新闻发布、数据报告、媒体演示
**描述：** 色块切割、标题极端放大、杂志排版实验感
**视觉特征：** 红色块穿插、超大标题字、高对比黑白

## 3. Startup VC Pitch — 硅谷路演风
**场景：** 路演、融资演示、增长汇报
**描述：** 超大指标数字、极度克制信息密度
**视觉特征：** 数字极大字重、墨绿点缀、极简数据可视化

## 4. Linear App — 极简暗色调
**场景：** 产品发布、SaaS 演示、技术展示
**描述：** 极简暗色调、精密工程感
**视觉特征：** 深空黑底、紫色系精密网格、精密等宽字

## 5. Vercel / Developer Dark — 开发者极简
**场景：** 开发者大会、技术文档、开源项目
**描述：** 纯黑开发者极简、精密网格、冷白代码感
**视觉特征：** 纯黑底、精密网格线、冷白等宽代码字

## 6. NASA / Scientific — 航天技术文档
**场景：** 科研汇报、航天演示、数据白皮书
**描述：** 航天技术文档、海军蓝底、数据可视化优先
**视觉特征：** 海军蓝底、精密数据图表、网格线背景

## 7. Glassmorphism UI — 磨砂玻璃卡片
**场景：** iOS 应用展示、visionOS 演示
**描述：** 磨砂玻璃卡片、渐变光感底层
**视觉特征：** 毛玻璃效果、多层渐变叠加、柔和阴影

## 8. Framer — 动态感布局
**场景：** 设计工具演示、创意展示
**描述：** 动态感布局、大胆渐变配色
**视觉特征：** 大胆撞色渐变、斜切布局、动态阴影

## 9. Figma Community — 活泼彩色
**场景：** 设计社区分享、创意展示
**描述：** 活泼彩色、设计师圈子美学
**视觉特征：** 高饱和彩色方块、拼贴感布局、设计师工具美学

## 10. Duotone Editorial — 摄影双色调
**场景：** 媒体内容、音乐推广、个人品牌
**描述：** 摄影双色调、Spotify 式媒体感
**视觉特征：** 双色叠加、图片为主、媒体封面感

## 11. Cyberpunk Neon — 赛博霓虹
**场景：** 游戏推荐、科技活动、创意展示
**描述：** 赛博霓虹、纯黑底青粉撞色
**视觉特征：** 霓虹发光线条、故障艺术感、RGB 偏移效果

## 12. Swiss Typography — 国际主义
**场景：** 平面设计展览、学术报告、博物馆
**描述：** 国际主义、纯排版驱动
**视觉特征：** 严格的网格系统、红黑经典配色、几何图形

## 13. Are.na Archive — 文化档案感
**场景：** 艺术展览、文化研究、档案展示
**描述：** 文化档案感、颗粒质感纹理
**视觉特征：** 颗粒噪点纹理、暖灰棕色系、老照片质感

## 14. Wabi-Sabi Organic — 侘寂工艺感
**场景：** 工艺品牌、陶瓷艺术、生活方式
**描述：** 侘寂工艺感、陶土色有机形状
**视觉特征：** 陶土粉笔色调、有机不规则形状、纸张纹理

## 15. Chinese Ink / 国风 — 水墨笔触
**场景：** 中国文化展示、国学研究、品牌东方美学
**描述：** 水墨笔触、宣纸白配朱红金、东方留白哲学
**视觉特征：** 墨色浓淡、朱红点缀、金色装饰、书法笔触

## 16. Teenage Engineering — 工业产品手册感
**场景：** 硬件产品、电子设备、技术品牌
**描述：** 工业产品手册感、橙色点缀、等宽技术排版
**视觉特征：** 橙黑撞色、等宽字体、精密网格、产品照片感

## 17. Muji / Japanese Ma — 日式「間」美学
**场景：** 无印品牌、生活方式、日本文化
**描述：** 日式「間」美学、负空间即设计、极致克制
**视觉特征：** 米白底、大量留白、极简产品图、和谐字体

## 18. Luxury Fashion House — 高奢品牌语言
**场景：** 奢侈品展示、高端品牌、活动邀请
**描述：** 高奢品牌语言、超宽字间距、冷峻衬线极简
**视觉特征：** 超宽字间距、衬线主导、金色/银色调、低调奢华

## 19. Stripe Press — 编辑排版风
**场景：** 出版内容、深度报告、思想领导力
**描述：** 编辑排版风、高对比度黑白
**视觉特征：** 高对比黑白、衬线主导、编辑出版物感

## 20. Apple Keynote Dark — 深空黑底
**场景：** 苹果产品发布、科技大会
**描述：** 深空黑底、产品发布质感
**视觉特征：** 纯黑背景、精密渐变、金属质感、高光效果

## 21. Academic Scholarly — 学术会议风
**场景：** 学术论文、学术会议、教育培训
**描述：** 学术会议风、象牙底多栏密排、严肃衬线体
**视觉特征：** 象牙白底、多栏布局、书名号引用、学术图表

## 22. Memphis Revival — 80 年代后现代
**场景：** 创意活动、时尚展览、80 年代主题
**描述：** 80 年代后现代复兴、几何图案、高饱和波普撞色
**视觉特征：** 几何图案、高饱和三原色撞色、波浪线条、网格背景

## 23. Brutalist Web — 反设计运动
**场景：** 创意设计、艺术展览、反主流文化
**描述：** 反设计运动、裸露结构、粗边框刻意未完成感
**视觉特征：** 粗黑边框、结构外露、高对比色块、无装饰感

## 24. Y2K Retro Digital — 千禧数字怀旧
**场景：** 复古科技、怀旧主题、数字艺术
**描述：** 千禧数字怀旧、铬金属渐变、CRT 扫描线噪点感
**视觉特征：** 铬金属渐变、彩虹光泽、网格透视、CRT 扫描线


path: skills/cjl-slides/scripts/extract-pptx.py
content: #!/usr/bin/env python3
"""
Extract all content from a PowerPoint file (.pptx).
Returns a JSON structure with slides, text, and images.

Usage:
    python extract-pptx.py <input.pptx> [output_dir]

Requires: pip install python-pptx
"""

import json
import os
import sys
from pptx import Presentation


def extract_pptx(file_path, output_dir="."):
    """
    Extract all content from a PowerPoint file.
    Returns a list of slide data dicts with text, images, and notes.
    """
    prs = Presentation(file_path)
    slides_data = []

    # Create assets directory for extracted images
    assets_dir = os.path.join(output_dir, "assets")
    os.makedirs(assets_dir, exist_ok=True)

    for slide_num, slide in enumerate(prs.slides):
        slide_data = {
            "number": slide_num + 1,
            "title": "",
            "content": [],
            "images": [],
            "notes": "",
        }

        for shape in slide.shapes:
            # Extract text content
            if shape.has_text_frame:
                if shape == slide.shapes.title:
                    slide_data["title"] = shape.text
                else:
                    slide_data["content"].append(
                        {"type": "text", "content": shape.text}
                    )

            # Extract images
            if shape.shape_type == 13:  # Picture type
                image = shape.image
                image_bytes = image.blob
                image_ext = image.ext
                image_name = f"slide{slide_num + 1}_img{len(slide_data['images']) + 1}.{image_ext}"
                image_path = os.path.join(assets_dir, image_name)

                with open(image_path, "wb") as f:
                    f.write(image_bytes)

                slide_data["images"].append(
                    {
                        "path": f"assets/{image_name}",
                        "width": shape.width,
                        "height": shape.height,
                    }
                )

        # Extract speaker notes
        if slide.has_notes_slide:
            notes_frame = slide.notes_slide.notes_text_frame
            slide_data["notes"] = notes_frame.text

        slides_data.append(slide_data)

    return slides_data


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python extract-pptx.py <input.pptx> [output_dir]")
        sys.exit(1)

    input_file = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else "."

    slides = extract_pptx(input_file, output_dir)

    # Write extracted data as JSON
    output_path = os.path.join(output_dir, "extracted-slides.json")
    with open(output_path, "w") as f:
        json.dump(slides, f, indent=2)

    print(f"Extracted {len(slides)} slides to {output_path}")
    for s in slides:
        img_count = len(s["images"])
        print(f"  Slide {s['number']}: {s['title'] or '(no title)'} — {img_count} image(s)")


path: skills/cjl-slides/scripts/html-to-pptx.py
content: #!/usr/bin/env python3
"""
html-to-pptx.py — 将 HTML 幻灯片转换为 PPTX 格式

保真原则：颜色（十六进制原样）、字体（精确字族）、字号（精确值）、布局（坐标还原）
用法：python html-to-pptx.py <input.html> [output.pptx]
"""

import sys
import re
import html
from pathlib import Path
from html.parser import HTMLParser
from pptx import Presentation
from pptx.util import Emu, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.oxml.ns import qn
from lxml import etree

# 默认 PPTX 尺寸：16:9 (914.4 x 514.35 cm -> 9144000 x 5143500 EMU)
SLIDE_WIDTH = Emu(9144000)
SLIDE_HEIGHT = Emu(5143500)


class StyleResolver:
    """从 HTML 解析计算后的样式"""

    def __init__(self):
        self.styles = {}
        self.compute_order = []

    def parse_color(self, color_str):
        """解析颜色字符串为 RGBColor"""
        if not color_str or color_str in ('inherit', 'transparent', 'none', ''):
            return None
        color_str = color_str.strip()
        if color_str.startswith('#'):
            if len(color_str) == 7:
                r = int(color_str[1:3], 16)
                g = int(color_str[3:5], 16)
                b = int(color_str[5:7], 16)
                return RGBColor(r, g, b)
            elif len(color_str) == 4:
                r = int(color_str[1] * 2, 16)
                g = int(color_str[2] * 2, 16)
                b = int(color_str[3] * 2, 16)
                return RGBColor(r, g, b)
        elif color_str.startswith('rgb'):
            match = re.findall(r'\d+', color_str)
            if len(match) >= 3:
                return RGBColor(int(match[0]), int(match[1]), int(match[2]))
        return None

    def parse_size(self, size_str, fallback=Pt(18)):
        """解析尺寸字符串为 Pt"""
        if not size_str or size_str in ('inherit', 'auto', ''):
            return fallback
        size_str = size_str.strip()
        match = re.match(r'([\d.]+)(px|em|rem|vw|vh|pt|cm)?', size_str)
        if not match:
            return fallback
        value = float(match.group(1))
        unit = match.group(2) or 'px'
        if unit == 'pt':
            return Pt(value)
        elif unit == 'px':
            return Pt(value * 0.75)
        elif unit == 'em' or unit == 'rem':
            return Pt(value * 16 * 0.75)
        elif unit == 'vw':
            return Pt(value * 9.144)
        elif unit == 'vh':
            return Pt(value * 5.1435)
        return fallback


class HtmlToPptx:
    """HTML 幻灯片 → PPTX 转换器"""

    def __init__(self, html_content: str):
        self.html = html_content
        self.resolver = StyleResolver()
        self.presentation = Presentation()
        self.presentation.slide_width = SLIDE_WIDTH
        self.presentation.slide_height = SLIDE_HEIGHT
        self._parse_html_tree()

    def _parse_html_tree(self):
        """用 lxml 解析 HTML"""
        try:
            root = etree.HTML(self.html)
        except Exception:
            root = etree.fromstring(f'<html><body>{self.html}</body></html>')
        self.root = root

    def _get_element_style(self, elem) -> dict:
        """从 style 属性解析内联样式"""
        style_attr = elem.get('style', '')
        style = {}
        for item in style_attr.split(';'):
            if ':' in item:
                key, value = item.split(':', 1)
                style[key.strip().lower()] = value.strip()
        return style

    def _elem_text(self, elem) -> str:
        """提取元素的所有文本内容（递归）"""
        if elem is None:
            return ''
        if isinstance(elem, str):
            return elem
        text = ''
        if elem.text:
            text += elem.text
        for child in elem:
            text += self._elem_text(child)
            if child.tail:
                text += child.tail
        return text

    def _tag_name(self, elem) -> str:
        if elem is None:
            return ''
        tag = elem.tag
        if isinstance(tag, str):
            return tag.lower()
        return ''

    def convert(self, output_path: str):
        """执行转换"""
        slides = self.root.xpath('//div[contains(concat(" ", normalize-space(@class), " "), " slide ")]')
        if not slides:
            slides = self.root.xpath('//section')
        if not slides:
            slides = self.root.xpath('//div[@id]')

        if not slides:
            blank_slide = self.presentation.slides.add_slide(self.presentation.slide_layouts[6])
            body = blank_slide.shapes.placeholders[0]
            tf = body.text_frame
            tf.text = self._elem_text(self.root)
        else:
            for slide_elem in slides:
                self._convert_slide(slide_elem)

        self.presentation.save(output_path)
        return output_path

    def _convert_slide(self, slide_elem):
        """转换单张幻灯片"""
        blank_slide_layout = self.presentation.slide_layouts[6]
        slide = self.presentation.slides.add_slide(blank_slide_layout)

        bg_shape = slide.shapes.add_shape(
            1, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT
        )
        bg_shape.line.fill.background()

        bg_style = self._get_element_style(slide_elem)
        if 'background-color' in bg_style:
            bg_color = self.resolver.parse_color(bg_style['background-color'])
            if bg_color:
                bg_shape.fill.solid()
                bg_shape.fill.fore_color.rgb = bg_color
        elif 'background' in bg_style:
            bg_color = self.resolver.parse_color(bg_style['background'])
            if bg_color:
                bg_shape.fill.solid()
                bg_shape.fill.fore_color.rgb = bg_color

        self._add_children_to_slide(slide, slide_elem, 0, 0, SLIDE_WIDTH, SLIDE_HEIGHT)

    def _add_children_to_slide(self, slide, parent, x, y, width, height):
        """递归将子元素添加到幻灯片"""
        children = list(parent)

        for child in children:
            tag = self._tag_name(child)

            if tag in ('style', 'script', 'meta', 'link'):
                continue

            style = self._get_element_style(child)

            left = x + self._parse_emu(style.get('left', '0'), width)
            top = y + self._parse_emu(style.get('top', '0'), height)
            child_width = self._parse_emu(style.get('width', f'{width}'), width)
            child_height = self._parse_emu(style.get('height', '0'), height)

            if tag == 'div' or tag == 'section':
                self._add_children_to_slide(slide, child, left, top, child_width, child_height)
            elif tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span'):
                self._add_text_to_slide(slide, child, left, top, child_width, child_height, style)
            elif tag == 'img':
                self._add_image_to_slide(slide, child, left, top, child_width, child_height)
            elif tag == 'svg':
                pass

    def _parse_emu(self, value, parent_value):
        """解析为 EMU 单位"""
        if not value or value in ('auto', 'inherit', ''):
            return 0
        value = value.strip()
        match = re.match(r'([\d.]+)(px|em|rem|vw|vh|pt|cm|%)?', value)
        if not match:
            return 0
        num = float(match.group(1))
        unit = match.group(2) or 'px'
        if unit == 'px':
            return int(num * 914400 / 96)
        elif unit == 'pt':
            return int(num * 12700)
        elif unit == 'em' or unit == 'rem':
            return int(num * 16 * 914400 / 96)
        elif unit == 'vw':
            return int(num / 100 * 9144000)
        elif unit == 'vh':
            return int(num / 100 * 5143500)
        elif unit == 'cm':
            return int(num * 360000)
        elif unit == '%':
            return int(num / 100 * parent_value)
        return 0

    def _add_text_to_slide(self, slide, elem, x, y, width, height, style):
        """添加文本框"""
        if width < Emu(10000):
            width = Emu(914400)

        text = self._elem_text(elem)
        if not text.strip():
            return

        font_size = self.resolver.parse_size(style.get('font-size', '24pt'), Pt(24))
        font_bold = 'bold' in style.get('font-weight', '').lower()
        font_italic = 'italic' in style.get('font-style', '').lower()
        font_color = self.resolver.parse_color(style.get('color', '#000000'))
        text_align = style.get('text-align', 'left')
        font_family = style.get('font-family', 'Arial')
        line_height = style.get('line-height', '1.2')

        try:
            txBox = slide.shapes.add_textbox(x, y, width, max(height, Emu(500000)))
            tf = txBox.text_frame
            tf.word_wrap = True

            p = tf.paragraphs[0]
            run = p.add_run()
            run.text = text

            run.font.size = font_size
            run.font.bold = font_bold
            run.font.italic = font_italic
            if font_color:
                run.font.color.rgb = font_color
            run.font.name = self._clean_font_name(font_family)

            if text_align == 'center':
                p.alignment = PP_ALIGN.CENTER
            elif text_align == 'right':
                p.alignment = PP_ALIGN.RIGHT

            from pptx.util import Pt as PtUnit
            lh = float(line_height) if line_height else 1.2
            tf.paragraphs[0].line_spacing = PtUnit(font_size.pt * lh)
        except Exception:
            pass

    def _add_image_to_slide(self, slide, elem, x, y, width, height):
        """添加图片占位符（PPTX 不直接支持 SVG 内联）"""
        src = elem.get('src', '')
        alt = elem.get('alt', 'image')
        if not src:
            return

        try:
            from pptx.util import Inches
            pic = slide.shapes.add_picture(src, x, y, width, height)
        except Exception:
            shape = slide.shapes.add_shape(
                1, x, y, width, height
            )
            shape.fill.solid()
            shape.fill.fore_color.rgb = RGBColor(200, 200, 200)
            tf = shape.text_frame
            tf.text = f'[{alt}]'
            tf.paragraphs[0].alignment = PP_ALIGN.CENTER

    def _clean_font_name(self, font_family):
        """清理字体名称"""
        fonts = font_family.split(',')
        primary = fonts[0].strip().strip('"\'')
        replacements = {
            'playfair display': 'Playfair Display',
            'fraunces': 'Fraunces',
            'dm serif display': 'DM Serif Display',
            'cormorant garamond': 'Cormorant Garamond',
            'syne': 'Syne',
            'bebas neue': 'Bebas Neue',
            'dm sans': 'DM Sans',
            'outfit': 'Outfit',
            'figtree': 'Figtree',
            'epilogue': 'Epilogue',
            'noto serif sc': 'Noto Serif SC',
            'noto sans sc': 'Noto Sans SC',
            'arial': 'Arial',
            'helvetica': 'Helvetica',
        }
        lower = primary.lower()
        for key, val in replacements.items():
            if key in lower:
                return val
        return primary


def main():
    if len(sys.argv) < 2:
        print("用法: python html-to-pptx.py <input.html> [output.pptx]")
        sys.exit(1)

    input_html = sys.argv[1]
    output_pptx = sys.argv[2] if len(sys.argv) > 2 else str(Path(input_html).with_suffix('.pptx'))

    html_path = Path(input_html)
    if not html_path.exists():
        print(f"文件不存在: {input_html}")
        sys.exit(1)

    html_content = html_path.read_text(encoding='utf-8')

    try:
        from pptx import Presentation
    except ImportError:
        print("需要 python-pptx 库: pip install python-pptx")
        sys.exit(1)

    converter = HtmlToPptx(html_content)
    result_path = converter.convert(output_pptx)
    print(f"PPTX 已保存: {result_path}")


if __name__ == '__main__':
    main()


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