chore: restructure skills repo with new agents and skill bundles
- Add new skills: deep-dive, docs-rag, meta-creator, ppt-maker, sdlc - Add agent configs: g-assistent, meta-creator, sdlc with prompt files - Add reference docs for custom agents and skills specification - Add utility scripts: install-agents.sh, orchestrate.py, puml2svg.sh - Update README and commit-message skill config - Remove deprecated skills: codereview, python, testing, typescript - Add .gitignore
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# ppt-maker
|
||||
|
||||
一键将 Markdown 转换为专业 PPT,支持自动图表生成和多种主题。
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
## Workflow
|
||||
|
||||

|
||||
|
||||
## 功能特性
|
||||
|
||||
- Markdown 语法驱动幻灯片布局(封面页、内容页、结束页)
|
||||
- 表格数据**自动转为图表**(饼图 / 柱状图 / 折线图)
|
||||
- 6 种内置主题:ocean、sunset、purple、luxury、midnight、classic
|
||||
- 支持有序/无序列表、引用块、代码块、表格
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
node skills/ppt-maker/scripts/ppt-maker.js -i input.md -o output.pptx -t ocean
|
||||
```
|
||||
|
||||
## 命令参数
|
||||
|
||||
| 参数 | 说明 | 必填 |
|
||||
|------|------|------|
|
||||
| `-i` | 输入 Markdown 文件 | ✅ |
|
||||
| `-o` | 输出 PPTX 文件 | ✅ |
|
||||
| `-t` | 主题名称(默认 ocean) | ❌ |
|
||||
| `-l` | 列出所有可用主题 | ❌ |
|
||||
|
||||
## Markdown 页面结构
|
||||
|
||||
```markdown
|
||||
# 封面标题 → 封面页
|
||||
副标题文字
|
||||
|
||||
## 章节标题 → 内容页
|
||||
## 感谢聆听 → 结束页(自动识别)
|
||||
```
|
||||
|
||||
## 图表自动生成
|
||||
|
||||
在标题中包含关键字,其下方表格自动转为对应图表:
|
||||
|
||||
| 图表类型 | 触发关键字示例 |
|
||||
|----------|--------------|
|
||||
| 🥧 饼图 | 占比、比例、饼图、pie |
|
||||
| 📊 柱状图 | 对比、排名、销售额、bar |
|
||||
| 📈 折线图 | 趋势、增长、月度、line |
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
skills/ppt-maker/
|
||||
├── SKILL.md
|
||||
├── README.md # this file
|
||||
├── _meta.json
|
||||
├── assets/
|
||||
│ ├── workflow.puml
|
||||
│ └── ppt-maker-workflow.svg
|
||||
└── scripts/
|
||||
├── ppt-maker.js # 主脚本
|
||||
└── package.json # 依赖 pptxgenjs
|
||||
```
|
||||
|
||||
## 示例提示词
|
||||
|
||||
```
|
||||
请使用 ppt-maker 技能,为我生成一份"2026年度销售总结"的汇报型幻灯片,
|
||||
包含销售占比饼图、各产品销售额柱状图、月度趋势折线图,主题使用 midnight。
|
||||
```
|
||||
@@ -0,0 +1,286 @@
|
||||
|
||||
---
|
||||
name: ppt-maker
|
||||
description: "专业级PPT一键生成。Markdown创建幻灯片,支持自动图表(饼图/柱状图/折线图)、多主题、有序/无序列表、引用块、代码块、表格、感谢页自动识别。"
|
||||
---
|
||||
|
||||
# PPT Maker - 专业级PPT生成工具
|
||||
|
||||
使用 Markdown 自动创建精美 PPT,**表格数据自动转为图表**,支持多种主题和智能布局。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i input.md -o output.pptx -t ocean
|
||||
```
|
||||
|
||||
## 命令参数
|
||||
|
||||
| 参数 | 说明 | 必填 |
|
||||
|------|------|------|
|
||||
| `-i, --input` | 输入 Markdown 文件路径 | ✅ |
|
||||
| `-o, --output` | 输出 PPTX 文件路径(自动补 .pptx 后缀) | ✅ |
|
||||
| `-t, --theme` | 主题名称,默认 ocean | ❌ |
|
||||
| `-l, --list` | 列出所有可用主题 | ❌ |
|
||||
| `-h, --help` | 显示帮助信息 | ❌ |
|
||||
|
||||
## 支持的主题
|
||||
|
||||
| 主题 | 风格 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| ocean | 蓝色海洋 | 科技/专业 |
|
||||
| sunset | 橙红日落 | 温暖/创意 |
|
||||
| purple | 紫罗兰 | 创意/设计 |
|
||||
| luxury | 黑金奢华 | 高端/奢侈 |
|
||||
| midnight | 深夜暗色 | 演示/震撼 |
|
||||
| classic | 经典绿 | 商务/正式 |
|
||||
|
||||
## Markdown 语法对照
|
||||
|
||||
### 页面类型
|
||||
|
||||
```markdown
|
||||
# 大标题 → 封面页(第一张幻灯片)
|
||||
副标题文字 → 封面副标题
|
||||
|
||||
## 章节标题 → 内容页
|
||||
## 感谢聆听 → 结束页(自动居中大字布局)
|
||||
```
|
||||
|
||||
**结束页自动识别关键字:** 感谢、谢谢、thank、thanks、Q&A、问答、结束、The End、再见、联系方式
|
||||
|
||||
### 内容元素
|
||||
|
||||
```markdown
|
||||
### 小标题 → 页内加粗小标题
|
||||
|
||||
- 无序列表项1 → 带圆点的无序列表
|
||||
- 无序列表项2
|
||||
|
||||
1. 有序列表项1 → 带编号圆圈的有序列表
|
||||
2. 有序列表项2
|
||||
|
||||
> 引用文字 → 带左侧竖条的引用块
|
||||
|
||||
普通文字 → 正文段落
|
||||
```
|
||||
|
||||
### 表格
|
||||
|
||||
```markdown
|
||||
| 列1 | 列2 | 列3 |
|
||||
|-----|-----|-----|
|
||||
| 内容 | 内容 | 内容 |
|
||||
```
|
||||
|
||||
表格含数值列时**自动检测**是否转为图表(见下方图表规则)。不含数值或未匹配图表规则时,保持表格原样显示(含交替行底色)。
|
||||
|
||||
### 代码块
|
||||
|
||||
````markdown
|
||||
```python
|
||||
print("Hello World")
|
||||
```
|
||||
````
|
||||
|
||||
深色背景 + 等宽字体 + 圆角边框显示。
|
||||
|
||||
## ⭐ 自动图表生成
|
||||
|
||||
**核心功能:** 在 `##` 标题、`###` 小标题或表格前的正文中包含特定关键字,其下方的表格自动转为对应图表。
|
||||
|
||||
### 图表类型与关键字
|
||||
|
||||
| 图表类型 | 触发关键字 |
|
||||
|----------|-----------|
|
||||
| 🥧 饼图 | 饼图、饼状图、占比、比例、份额、构成、组成、百分比、比重、pie |
|
||||
| 📊 柱状图 | 柱状、柱状图、柱形、排名、top、对比、比较、分布、销售额、金额、数量、业绩、产量、营收、bar |
|
||||
| 📈 折线图 | 折线、折线图、趋势、增长、变化、走势、曲线、时间、月度、季度、年度、line、trend |
|
||||
|
||||
### 智能推断(无关键字时)
|
||||
|
||||
- 数值列总和在 80~120 之间 → 自动识别为**饼图**(占比数据)
|
||||
- 有 ≥2 个数值点 → 默认生成**柱状图**
|
||||
- 支持多列数值 → 自动生成**多系列**图表
|
||||
|
||||
### 数值解析
|
||||
|
||||
自动清理单元格中的干扰字符,以下写法均可正确识别:
|
||||
- `100万` `¥250` `$1,200` `30%` `85元` `1200亿`
|
||||
|
||||
### 图表示例
|
||||
|
||||
#### 饼图示例
|
||||
|
||||
```markdown
|
||||
## 销售占比分析
|
||||
### 各产品销售占比饼图
|
||||
| 产品 | 占比(%) |
|
||||
|------|---------|
|
||||
| 大米 | 30 |
|
||||
| 高粱 | 50 |
|
||||
| 小麦 | 20 |
|
||||
```
|
||||
|
||||
#### 柱状图示例
|
||||
|
||||
```markdown
|
||||
## 各产品销售额对比
|
||||
### 年度销售额柱状图
|
||||
| 产品 | 销售额(万元) |
|
||||
|------|-------------|
|
||||
| 大米 | 100 |
|
||||
| 高粱 | 250 |
|
||||
| 小麦 | 130 |
|
||||
```
|
||||
|
||||
#### 折线图示例
|
||||
|
||||
```markdown
|
||||
## 月度销售趋势
|
||||
### 销售额变化趋势折线图
|
||||
| 月份 | 销售额(万元) |
|
||||
|------|-------------|
|
||||
| 1月 | 35 |
|
||||
| 2月 | 42 |
|
||||
| 3月 | 58 |
|
||||
| 4月 | 72 |
|
||||
```
|
||||
|
||||
#### 不转图表的表格(无数值列或非图表场景)
|
||||
|
||||
```markdown
|
||||
## 工作计划
|
||||
| 季度 | 目标 | 负责人 |
|
||||
|------|------|--------|
|
||||
| Q1 | 完成招聘 | 张经理 |
|
||||
| Q2 | 市场拓展 | 李经理 |
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
提供markdown文件,比如input.md,然后通过指令生成
|
||||
```bash
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i input.md -o output.pptx -t ocean
|
||||
```
|
||||
### markdown提示词示例
|
||||
```markdown
|
||||
# 2026年度总结报告
|
||||
北灵聊AI · 年度工作汇报
|
||||
|
||||
## 销售占比分析
|
||||
### 各产品占比饼图
|
||||
| 产品 | 占比(%) |
|
||||
|------|---------|
|
||||
| 大米 | 30 |
|
||||
| 高粱 | 40 |
|
||||
| 小麦 | 20 |
|
||||
| 玉米 | 10 |
|
||||
|
||||
## 销售额对比
|
||||
### 各产品销售额柱状图
|
||||
| 产品 | 销售额(万元) |
|
||||
|------|-------------|
|
||||
| 大米 | 100 |
|
||||
| 高粱 | 250 |
|
||||
| 小麦 | 130 |
|
||||
|
||||
## 月度趋势
|
||||
### 全年销售额变化趋势折线图
|
||||
| 月份 | 销售额(万元) |
|
||||
|------|-------------|
|
||||
| 1月 | 35 |
|
||||
| 6月 | 72 |
|
||||
| 12月 | 102 |
|
||||
|
||||
## 核心成果
|
||||
### 业务拓展
|
||||
- 新增客户 126 家,同比增长 35%
|
||||
- 开拓西南市场,覆盖 4 个新省份
|
||||
|
||||
### 团队建设
|
||||
1. 团队扩充至 28 人
|
||||
2. 组织培训 12 场
|
||||
3. 员工满意度达 92%
|
||||
|
||||
> 全年目标超额完成,总销售额突破 560 万元
|
||||
|
||||
## 感谢聆听
|
||||
北灵聊AI
|
||||
期待2027再创佳绩!
|
||||
```
|
||||
|
||||
|
||||
### 自然语言提示词示例
|
||||
```
|
||||
请使用 ppt-maker 技能,为我生成一份“2026生成式AI行业发展与企业落地趋势”的汇报型幻灯片,整体风格专业、简洁、科技感强,讲解人是北灵聊AI。
|
||||
|
||||
封面是2026生成式AI行业发展与企业落地趋势,副标题写“模型能力升级、企业应用加速与商业化观察”,并显示讲解人“北灵聊AI”。
|
||||
|
||||
第一页是企业采用生成式AI的主要应用场景分布,用饼状图展示,其中知识助手占比32,智能客服占比24,内容生成占比18,研发提效占比16,数据分析占比10。
|
||||
|
||||
第二页是企业AI项目预算投入对比,用柱状图展示,其中大模型平台建设预算380万,AI代码助手预算300万,AI办公助手预算260万,AI智能客服预算220万,AI营销内容生成预算180万。
|
||||
|
||||
第三页是2026年企业生成式AI项目推进热度趋势,用折线图展示,其中Q1热度指数48,Q2热度指数63,Q3热度指数78,Q4热度指数92。
|
||||
|
||||
最后一页是感谢,标题写“感谢聆听”,副标题写“欢迎交流生成式AI与企业应用实践”。
|
||||
```
|
||||
|
||||
## 支持的命令行
|
||||
|
||||
```bash
|
||||
# 查看帮助
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -h
|
||||
|
||||
# 列出所有主题
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -l
|
||||
|
||||
# 海洋蓝主题(默认)
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i slides.md -o demo.pptx
|
||||
|
||||
# 深夜科技主题
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i slides.md -o demo.pptx -t midnight
|
||||
|
||||
# 黑金奢华主题
|
||||
node ~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js -i slides.md -o demo.pptx -t luxury
|
||||
```
|
||||
|
||||
|
||||
## 布局特点
|
||||
|
||||
### 页面类型
|
||||
|
||||
- **封面页** — 大标题 + 副标题 + 左侧装饰竖条
|
||||
- **内容页** — 标题栏背景 + 正文区域 + 页码
|
||||
- **结束页** — 居中大字 + 装饰色块 + 上下装饰线
|
||||
|
||||
### 装饰元素
|
||||
|
||||
- 顶部主题色强调条
|
||||
- 左侧边栏装饰线
|
||||
- 标题栏浅色背景
|
||||
- 封面竖线装饰 + 底部横线
|
||||
|
||||
### 内容渲染
|
||||
|
||||
- 无序列表:主题色圆点
|
||||
- 有序列表:主题色编号圆圈
|
||||
- 引用块:左侧竖条 + 浅色背景 + 斜体
|
||||
- 代码块:深色背景 + 等宽字体 + 圆角
|
||||
- 表格:表头底色 + 交替行着色
|
||||
- 图表与剩余内容并排显示(饼图右侧/柱状折线收窄后右侧)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Markdown格式要求:** 必须用 `#` 开头作为封面页,`##` 开头分页
|
||||
2. **图表触发:** 关键字写在 `##` 标题或 `###` 小标题中最可靠
|
||||
3. **表格格式:** 第一行为表头,第二行为分隔行 `|---|---|`,第三行起为数据
|
||||
4. **数值列:** 表格第二列起含可解析数字才会触发图表
|
||||
5. **输出格式:** 自动补 `.pptx` 后缀
|
||||
6. **行内格式:** `**粗体**` `*斜体*` `~~删除线~~` 等会自动清理为纯文本
|
||||
|
||||
## 文件位置
|
||||
|
||||
- 脚本:`~/.openclaw/workspace/skills/ppt-maker/scripts/ppt-maker.js`
|
||||
- 依赖:`pptxgenjs`(已安装)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn75v9attg6retx4mdm14beva983desk",
|
||||
"slug": "ppt-maker",
|
||||
"version": "1.0.3",
|
||||
"publishedAt": 1774166054605
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@startuml ppt-maker-architecture
|
||||
skinparam componentStyle rectangle
|
||||
skinparam defaultFontName Arial
|
||||
skinparam backgroundColor #FAFAFA
|
||||
|
||||
package "ppt-maker Skill" {
|
||||
component "SKILL.md\n(instructions)" as SKILL
|
||||
component "scripts/ppt-maker.js\n(core engine)" as ENGINE
|
||||
component "scripts/package.json\n(deps: pptxgenjs)" as PKG
|
||||
}
|
||||
|
||||
package "ppt-maker.js Internals" {
|
||||
component "Markdown Parser\n(slides, headings, lists)" as PARSER
|
||||
component "Chart Detector\n(keyword → pie/bar/line)" as CHART
|
||||
component "Theme Engine\n(ocean/sunset/purple/...)" as THEME
|
||||
component "PPTX Renderer\n(pptxgenjs)" as RENDERER
|
||||
}
|
||||
|
||||
actor User
|
||||
|
||||
User --> SKILL : natural language request
|
||||
SKILL --> ENGINE : node ppt-maker.js -i ... -o ... -t ...
|
||||
ENGINE --> PARSER
|
||||
ENGINE --> CHART
|
||||
ENGINE --> THEME
|
||||
PARSER --> RENDERER
|
||||
CHART --> RENDERER
|
||||
THEME --> RENDERER
|
||||
RENDERER --> User : output.pptx
|
||||
@enduml
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.5 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.8 KiB |
@@ -0,0 +1,25 @@
|
||||
@startuml ppt-maker-workflow
|
||||
skinparam defaultFontName Arial
|
||||
skinparam backgroundColor #FAFAFA
|
||||
|
||||
actor User
|
||||
participant "ppt-maker\nSkill" as SKILL
|
||||
participant "ppt-maker.js" as ENGINE
|
||||
|
||||
User -> SKILL : "生成一份PPT: <描述>"
|
||||
SKILL -> SKILL : generate Markdown content
|
||||
SKILL -> ENGINE : node ppt-maker.js -i input.md -o out.pptx -t <theme>
|
||||
|
||||
ENGINE -> ENGINE : parse slides\n(# → cover, ## → content, ## 感谢 → end)
|
||||
|
||||
loop each ## slide
|
||||
ENGINE -> ENGINE : scan headings for chart keywords
|
||||
alt chart keyword found & table present
|
||||
ENGINE -> ENGINE : render pie / bar / line chart
|
||||
else
|
||||
ENGINE -> ENGINE : render text / list / table
|
||||
end
|
||||
end
|
||||
|
||||
ENGINE --> User : output.pptx
|
||||
@enduml
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scripts",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pptxgenjs": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/https": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
|
||||
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"queue": "6.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/pptxgenjs": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
|
||||
"integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"https": "^1.0.0",
|
||||
"image-size": "^1.2.1",
|
||||
"jszip": "^3.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ppt-maker",
|
||||
"version": "1.0.0",
|
||||
"description": "Generate PPT slides automatically from user input",
|
||||
"main": "ppt-maker.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": ["ppt", "pptx", "slides", "presentation", "ai", "generator", "automatic"],
|
||||
"author": "北灵聊AI",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pptxgenjs": "^4.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
/**
|
||||
* PPT Maker - Markdown to PPTX Generator
|
||||
* Supports auto charts (pie/bar/line), multiple themes, ending page detection.
|
||||
*/
|
||||
|
||||
var PptxGenJS = require("pptxgenjs");
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 1. Themes & Colors (no # prefix)
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
var THEMES = {
|
||||
sunset: { bg: 'FFF8F3', title: 'E85D04', text: '3D405B', accent: 'F48C06', secondary: 'FAA307', light: 'FFECD2', lighter: 'FFF5EB' },
|
||||
ocean: { bg: 'F0F8FF', title: '0077B6', text: '2D3748', accent: '00B4D8', secondary: '90E0EF', light: 'CAF0F8', lighter: 'E8F8FF' },
|
||||
purple: { bg: 'FAF5FF', title: '7C3AED', text: '4C1D95', accent: 'A78BFA', secondary: 'C4B5FD', light: 'EDE9FE', lighter: 'F5F3FF' },
|
||||
luxury: { bg: '1C1917', title: 'F5F5F4', text: 'A8A29E', accent: 'D4AF37', secondary: 'F59E0B', light: '292524', lighter: '1C1917' },
|
||||
midnight: { bg: '0F172A', title: 'F8FAFC', text: 'CBD5E1', accent: '38BDF8', secondary: '60A5FA', light: '1E293B', lighter: '0F172A' },
|
||||
classic: { bg: 'FFFFFF', title: '1F2937', text: '4B5563', accent: '059669', secondary: '10B981', light: 'ECFDF5', lighter: 'F0FDF4' }
|
||||
};
|
||||
|
||||
var CHART_COLORS = {
|
||||
ocean: ['0077B6', '00B4D8', '90E0EF', '48CAE4', '023E8A', '0096C7', 'ADE8F4'],
|
||||
sunset: ['E85D04', 'F48C06', 'FAA307', 'FFBA08', 'DC2F02', 'E36414', 'F77F00'],
|
||||
purple: ['7C3AED', 'A78BFA', 'C4B5FD', '8B5CF6', '6D28D9', '5B21B6', 'DDD6FE'],
|
||||
luxury: ['D4AF37', 'F59E0B', 'FBBF24', 'FFD700', 'B8860B', 'D97706', 'FCD34D'],
|
||||
midnight: ['38BDF8', '60A5FA', '93C5FD', '2563EB', '1D4ED8', '3B82F6', 'BFDBFE'],
|
||||
classic: ['059669', '10B981', '34D399', '6EE7B7', '047857', '065F46', 'A7F3D0']
|
||||
};
|
||||
|
||||
var CHART_RULES = [
|
||||
{ type: 'pie', keys: ['饼图', '饼状图', '占比', '比例', '份额', '构成', '组成', '百分比', '比重', 'pie', 'proportion', 'share'] },
|
||||
{ type: 'line', keys: ['折线', '折线图', '趋势', '增长', '变化', '走势', '曲线', '时间', '月度', '季度', '年度', 'line', 'trend'] },
|
||||
{ type: 'bar', keys: ['柱状', '柱状图', '柱形', '柱形图', '排名', 'top', '对比', '比较', '分布', '销售额', '金额', '数量', '业绩', '产量', '营收', '收入', 'bar', 'column', 'chart'] }
|
||||
];
|
||||
|
||||
var ENDING_KEYWORDS = ['感谢', '谢谢', 'thank', 'thanks', 'q&a', 'q & a', '问答', '结束', 'the end', '再见', '联系方式', '联系我们'];
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 2. Utility Functions
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function stripInlineMarkdown(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/__(.+?)__/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/_(.+?)_/g, '$1')
|
||||
.replace(/~~(.+?)~~/g, '$1')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/\[(.+?)\]\(.+?\)/g, '$1')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isTableSeparator(line) {
|
||||
var inner = line.replace(/^\|/, '').replace(/\|$/, '');
|
||||
var cells = inner.split('|');
|
||||
return cells.length > 0 && cells.every(function(c) {
|
||||
return /^\s*:?-{2,}:?\s*$/.test(c);
|
||||
});
|
||||
}
|
||||
|
||||
function isEndingSlide(title) {
|
||||
if (!title) return false;
|
||||
var lower = title.toLowerCase().trim();
|
||||
return ENDING_KEYWORDS.some(function(k) { return lower.indexOf(k) !== -1; });
|
||||
}
|
||||
|
||||
function parseNumericCell(raw) {
|
||||
if (!raw) return NaN;
|
||||
var cleaned = raw
|
||||
.replace(/[,,]/g, '')
|
||||
.replace(/[%%]/g, '')
|
||||
.replace(/[¥¥$€£₩]/g, '')
|
||||
.replace(/[元万亿千百十个份人次件台套组年月日号]/g, '')
|
||||
.replace(/[(())\s]/g, '')
|
||||
.trim();
|
||||
return parseFloat(cleaned);
|
||||
}
|
||||
|
||||
function ensureColors(colors, needed) {
|
||||
var result = [];
|
||||
for (var i = 0; i < needed; i++) {
|
||||
result.push(colors[i % colors.length]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 3. Markdown Parser
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function parse(text) {
|
||||
var slides = [];
|
||||
var lines = text.split('\n');
|
||||
var current = null;
|
||||
var inCode = false;
|
||||
var codeContent = [];
|
||||
|
||||
for (var li = 0; li < lines.length; li++) {
|
||||
var line = lines[li];
|
||||
var trimmed = line.trim();
|
||||
|
||||
if (trimmed.indexOf('```') === 0) {
|
||||
if (inCode) {
|
||||
if (current) {
|
||||
current.content.push({ type: 'code', code: codeContent.join('\n') });
|
||||
}
|
||||
codeContent = [];
|
||||
}
|
||||
inCode = !inCode;
|
||||
continue;
|
||||
}
|
||||
if (inCode) {
|
||||
codeContent.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (/^# (?!#)/.test(trimmed)) {
|
||||
if (current) slides.push(current);
|
||||
current = {
|
||||
type: 'cover',
|
||||
title: stripInlineMarkdown(trimmed.slice(2)),
|
||||
subtitle: '',
|
||||
content: []
|
||||
};
|
||||
}
|
||||
else if (/^## (?!#)/.test(trimmed)) {
|
||||
if (current) slides.push(current);
|
||||
var title = stripInlineMarkdown(trimmed.slice(3));
|
||||
current = {
|
||||
type: isEndingSlide(title) ? 'ending' : 'content',
|
||||
title: title,
|
||||
content: []
|
||||
};
|
||||
}
|
||||
else if (trimmed.indexOf('### ') === 0) {
|
||||
if (!current) current = { type: 'content', title: '', content: [] };
|
||||
current.content.push({ type: 'h3', text: stripInlineMarkdown(trimmed.slice(4)) });
|
||||
}
|
||||
else if (/^[-*]\s/.test(trimmed)) {
|
||||
if (!current) current = { type: 'content', title: '', content: [] };
|
||||
var itemText = stripInlineMarkdown(trimmed.replace(/^[-*]\s+/, ''));
|
||||
var last = current.content[current.content.length - 1];
|
||||
if (last && last.type === 'list') {
|
||||
last.items.push(itemText);
|
||||
} else {
|
||||
current.content.push({ type: 'list', items: [itemText] });
|
||||
}
|
||||
}
|
||||
else if (/^\d+\.\s/.test(trimmed)) {
|
||||
if (!current) current = { type: 'content', title: '', content: [] };
|
||||
var oItemText = stripInlineMarkdown(trimmed.replace(/^\d+\.\s+/, ''));
|
||||
var oLast = current.content[current.content.length - 1];
|
||||
if (oLast && oLast.type === 'olist') {
|
||||
oLast.items.push(oItemText);
|
||||
} else {
|
||||
current.content.push({ type: 'olist', items: [oItemText] });
|
||||
}
|
||||
}
|
||||
else if (trimmed.charAt(0) === '|') {
|
||||
if (isTableSeparator(trimmed)) continue;
|
||||
if (!current) current = { type: 'content', title: '', content: [] };
|
||||
var inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
|
||||
var cells = inner.split('|').map(function(c) { return c.trim(); });
|
||||
if (cells.length === 0) continue;
|
||||
var tLast = current.content[current.content.length - 1];
|
||||
if (tLast && tLast.type === 'table') {
|
||||
tLast.rows.push(cells);
|
||||
} else {
|
||||
current.content.push({ type: 'table', rows: [cells] });
|
||||
}
|
||||
}
|
||||
else if (trimmed.charAt(0) === '>') {
|
||||
if (!current) current = { type: 'content', title: '', content: [] };
|
||||
var quoteText = stripInlineMarkdown(trimmed.replace(/^>\s*/, ''));
|
||||
var qLast = current.content[current.content.length - 1];
|
||||
if (qLast && qLast.type === 'quote') {
|
||||
qLast.lines.push(quoteText);
|
||||
} else {
|
||||
current.content.push({ type: 'quote', lines: [quoteText] });
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!current) current = { type: 'content', title: '', content: [] };
|
||||
var cleaned = stripInlineMarkdown(trimmed);
|
||||
if (current.type === 'cover' && !current.subtitle) {
|
||||
current.subtitle = cleaned;
|
||||
} else {
|
||||
current.content.push({ type: 'text', text: cleaned });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inCode && codeContent.length > 0 && current) {
|
||||
current.content.push({ type: 'code', code: codeContent.join('\n') });
|
||||
}
|
||||
if (current) slides.push(current);
|
||||
return slides;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 4. Chart Detection
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function extractSeries(table) {
|
||||
if (!table.rows || table.rows.length < 2) return null;
|
||||
|
||||
var headers = table.rows[0];
|
||||
var dataRows = table.rows.slice(1);
|
||||
if (headers.length < 2 || dataRows.length === 0) return null;
|
||||
|
||||
var labels = dataRows.map(function(r) { return (r[0] || '').trim(); });
|
||||
var series = [];
|
||||
|
||||
for (var col = 1; col < headers.length; col++) {
|
||||
var values = dataRows.map(function(r) { return parseNumericCell(r[col]); });
|
||||
if (values.every(function(v) { return !isNaN(v) && isFinite(v); })) {
|
||||
series.push({
|
||||
name: (headers[col] || '').trim() || ('Series' + col),
|
||||
labels: labels,
|
||||
values: values
|
||||
});
|
||||
}
|
||||
}
|
||||
return series.length > 0 ? series : null;
|
||||
}
|
||||
|
||||
function detectChart(table, slide, tableIndex) {
|
||||
var series = extractSeries(table);
|
||||
if (!series) return null;
|
||||
|
||||
var labels = series[0].labels;
|
||||
var hints = [];
|
||||
|
||||
if (slide.title) hints.push(slide.title.toLowerCase());
|
||||
|
||||
var hintIndex = -1;
|
||||
for (var j = tableIndex - 1; j >= 0; j--) {
|
||||
var item = slide.content[j];
|
||||
if (item.type === 'h3') {
|
||||
hints.push(item.text.toLowerCase());
|
||||
hintIndex = j;
|
||||
break;
|
||||
}
|
||||
if (item.type === 'text') {
|
||||
hints.push(item.text.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
hints.push(table.rows[0].map(function(h) { return (h || '').toLowerCase(); }).join(' '));
|
||||
|
||||
var combined = hints.join(' ');
|
||||
|
||||
for (var ri = 0; ri < CHART_RULES.length; ri++) {
|
||||
var rule = CHART_RULES[ri];
|
||||
if (rule.keys.some(function(k) { return combined.indexOf(k) !== -1; })) {
|
||||
return { type: rule.type, series: series, labels: labels, hintIndex: hintIndex };
|
||||
}
|
||||
}
|
||||
|
||||
var vals = series[0].values;
|
||||
var sum = vals.reduce(function(a, b) { return a + b; }, 0);
|
||||
var count = vals.length;
|
||||
|
||||
if (count >= 2 && count <= 12 && sum >= 80 && sum <= 120) {
|
||||
return { type: 'pie', series: series, labels: labels, hintIndex: hintIndex };
|
||||
}
|
||||
if (count >= 2) {
|
||||
return { type: 'bar', series: series, labels: labels, hintIndex: hintIndex };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 5. Chart Type Resolution (multi-version compat)
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function getChartType(pres, name) {
|
||||
var MAP = { pie: 'PIE', line: 'LINE', bar: 'BAR' };
|
||||
var key = MAP[name];
|
||||
if (!key) return name;
|
||||
if (pres.charts && pres.charts[key] !== undefined) return pres.charts[key];
|
||||
if (pres.ChartType && pres.ChartType[key] !== undefined) return pres.ChartType[key];
|
||||
if (pres.ChartType && pres.ChartType[name] !== undefined) return pres.ChartType[name];
|
||||
return name;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 6. Chart Rendering
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function addChartToSlide(s, pres, chartData, colors, t, layout) {
|
||||
var lx = (layout && layout.x) || 0.5;
|
||||
var ly = (layout && layout.y) || 1.4;
|
||||
var lw = (layout && layout.w) || 9;
|
||||
var lh = (layout && layout.h) || 3.8;
|
||||
|
||||
var chartType = getChartType(pres, chartData.type);
|
||||
var isPie = chartData.type === 'pie';
|
||||
var isLine = chartData.type === 'line';
|
||||
|
||||
var data = isPie ? [chartData.series[0]] : chartData.series;
|
||||
var needed = isPie ? data[0].values.length : Math.max(data[0].values.length, data.length);
|
||||
var clrs = ensureColors(colors, needed);
|
||||
|
||||
var opts = {
|
||||
x: lx, y: ly, w: lw, h: lh,
|
||||
chartColors: clrs,
|
||||
showLegend: true,
|
||||
legendPos: isPie ? 'r' : 'b',
|
||||
legendFontSize: 9,
|
||||
legendColor: t.text,
|
||||
showTitle: false
|
||||
};
|
||||
|
||||
if (isPie) {
|
||||
opts.showPercent = true;
|
||||
opts.showValue = false;
|
||||
opts.dataLabelColor = t.text;
|
||||
opts.dataLabelFontSize = 10;
|
||||
} else if (isLine) {
|
||||
opts.lineSize = 2;
|
||||
opts.showMarker = true;
|
||||
opts.markerSize = 6;
|
||||
opts.catAxisLabelColor = t.text;
|
||||
opts.catAxisLabelFontSize = 9;
|
||||
opts.valAxisLabelColor = t.text;
|
||||
opts.valAxisLabelFontSize = 9;
|
||||
opts.showValue = true;
|
||||
opts.dataLabelColor = t.text;
|
||||
opts.dataLabelFontSize = 8;
|
||||
opts.dataLabelPosition = 'outEnd';
|
||||
} else {
|
||||
opts.barDir = 'col';
|
||||
opts.barGapWidthPct = 80;
|
||||
opts.catAxisLabelColor = t.text;
|
||||
opts.catAxisLabelFontSize = 10;
|
||||
opts.valAxisLabelColor = t.text;
|
||||
opts.valAxisLabelFontSize = 9;
|
||||
opts.showValue = true;
|
||||
opts.dataLabelColor = t.text;
|
||||
opts.dataLabelFontSize = 9;
|
||||
opts.dataLabelPosition = 'outEnd';
|
||||
}
|
||||
|
||||
s.addChart(chartType, data, opts);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 7. Content Rendering
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function renderTable(s, tableItem, t, startY, maxY, startX, totalW) {
|
||||
if (!tableItem.rows || tableItem.rows.length === 0) return startY;
|
||||
|
||||
var colCount = Math.max.apply(null, tableItem.rows.map(function(r) { return r.length; }));
|
||||
var cw = totalW / colCount;
|
||||
var rowH = 0.35;
|
||||
|
||||
for (var r = 0; r < tableItem.rows.length; r++) {
|
||||
var ry = startY + r * rowH;
|
||||
if (ry + rowH > maxY) break;
|
||||
|
||||
if (r === 0) {
|
||||
s.addShape('rect', {
|
||||
x: startX - 0.05, y: ry, w: colCount * cw + 0.1, h: rowH,
|
||||
fill: { color: t.light }
|
||||
});
|
||||
} else if (r % 2 === 0) {
|
||||
s.addShape('rect', {
|
||||
x: startX - 0.05, y: ry, w: colCount * cw + 0.1, h: rowH,
|
||||
fill: { color: t.lighter || t.bg }
|
||||
});
|
||||
}
|
||||
|
||||
for (var c = 0; c < tableItem.rows[r].length; c++) {
|
||||
s.addText(tableItem.rows[r][c], {
|
||||
x: startX + c * cw, y: ry, w: cw - 0.05, h: rowH,
|
||||
fontSize: 10, color: r === 0 ? t.title : t.text,
|
||||
fontFace: 'Arial', bold: r === 0, valign: 'middle'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var renderedRows = Math.min(tableItem.rows.length, Math.floor((maxY - startY) / rowH));
|
||||
return startY + renderedRows * rowH + 0.2;
|
||||
}
|
||||
|
||||
function renderContent(s, content, t, opts) {
|
||||
var startY = (opts && opts.startY) || 1.4;
|
||||
var maxY = (opts && opts.maxY) || 5.0;
|
||||
var x = (opts && opts.x) || 0.4;
|
||||
var w = (opts && opts.w) || 8.5;
|
||||
var y = startY;
|
||||
|
||||
for (var idx = 0; idx < content.length; idx++) {
|
||||
var item = content[idx];
|
||||
if (y > maxY) break;
|
||||
|
||||
if (item.type === 'h3') {
|
||||
s.addText(item.text, {
|
||||
x: x, y: y, w: w, h: 0.4,
|
||||
fontSize: 16, color: t.title, fontFace: 'Arial', bold: true
|
||||
});
|
||||
y += 0.5;
|
||||
}
|
||||
else if (item.type === 'list') {
|
||||
for (var li = 0; li < item.items.length; li++) {
|
||||
if (y > maxY) break;
|
||||
s.addShape('ellipse', {
|
||||
x: x + 0.02, y: y + 0.13, w: 0.09, h: 0.09,
|
||||
fill: { color: t.accent }
|
||||
});
|
||||
s.addText(item.items[li], {
|
||||
x: x + 0.22, y: y, w: w - 0.3, h: 0.35,
|
||||
fontSize: 13, color: t.text, fontFace: 'Arial'
|
||||
});
|
||||
y += 0.42;
|
||||
}
|
||||
}
|
||||
else if (item.type === 'olist') {
|
||||
for (var oi = 0; oi < item.items.length; oi++) {
|
||||
if (y > maxY) break;
|
||||
s.addShape('ellipse', {
|
||||
x: x, y: y + 0.05, w: 0.22, h: 0.22,
|
||||
fill: { color: t.accent }
|
||||
});
|
||||
s.addText(String(oi + 1), {
|
||||
x: x, y: y + 0.05, w: 0.22, h: 0.22,
|
||||
fontSize: 9, color: 'FFFFFF', fontFace: 'Arial', bold: true,
|
||||
align: 'center', valign: 'middle'
|
||||
});
|
||||
s.addText(item.items[oi], {
|
||||
x: x + 0.3, y: y, w: w - 0.4, h: 0.35,
|
||||
fontSize: 13, color: t.text, fontFace: 'Arial'
|
||||
});
|
||||
y += 0.42;
|
||||
}
|
||||
}
|
||||
else if (item.type === 'code') {
|
||||
var lineCount = item.code.split('\n').length;
|
||||
var ch = Math.min(2.5, lineCount * 0.22 + 0.3);
|
||||
s.addShape('roundRect', {
|
||||
x: x - 0.1, y: y, w: w + 0.2, h: ch,
|
||||
fill: { color: '1E1E1E' }, rectRadius: 0.05
|
||||
});
|
||||
s.addText(item.code, {
|
||||
x: x + 0.05, y: y + 0.1, w: w - 0.1, h: ch - 0.2,
|
||||
fontSize: 10, color: 'D4D4D4', fontFace: 'Consolas', valign: 'top'
|
||||
});
|
||||
y += ch + 0.2;
|
||||
}
|
||||
else if (item.type === 'quote') {
|
||||
var quoteText = item.lines.join('\n');
|
||||
var qLines = item.lines.length;
|
||||
var qh = Math.min(2.0, qLines * 0.25 + 0.2);
|
||||
s.addShape('rect', {
|
||||
x: x, y: y, w: 0.05, h: qh,
|
||||
fill: { color: t.accent }
|
||||
});
|
||||
s.addShape('rect', {
|
||||
x: x + 0.05, y: y, w: w - 0.05, h: qh,
|
||||
fill: { color: t.light }
|
||||
});
|
||||
s.addText(quoteText, {
|
||||
x: x + 0.2, y: y + 0.05, w: w - 0.3, h: qh - 0.1,
|
||||
fontSize: 12, color: t.text, fontFace: 'Arial', italic: true, valign: 'top'
|
||||
});
|
||||
y += qh + 0.15;
|
||||
}
|
||||
else if (item.type === 'table') {
|
||||
y = renderTable(s, item, t, y, maxY, x, w);
|
||||
}
|
||||
else if (item.type === 'text') {
|
||||
s.addText(item.text, {
|
||||
x: x, y: y, w: w, h: 0.35,
|
||||
fontSize: 12, color: t.text, fontFace: 'Arial'
|
||||
});
|
||||
y += 0.4;
|
||||
}
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 8. Slide Renderers
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function addDecorations(s, t) {
|
||||
s.addShape('rect', { x: 0, y: 0, w: 10, h: 0.12, fill: { color: t.accent } });
|
||||
s.addShape('rect', { x: 0, y: 0, w: 0.10, h: 5.625, fill: { color: t.accent } });
|
||||
}
|
||||
|
||||
function renderCoverSlide(s, slide, t) {
|
||||
s.addShape('rect', { x: 0.4, y: 1.3, w: 0.06, h: 1.6, fill: { color: t.accent } });
|
||||
s.addText(slide.title, {
|
||||
x: 0.7, y: 1.3, w: 8.5, h: 1.4,
|
||||
fontSize: 40, color: t.title, fontFace: 'Arial', bold: true, valign: 'middle'
|
||||
});
|
||||
if (slide.subtitle) {
|
||||
s.addText(slide.subtitle, {
|
||||
x: 0.7, y: 3.0, w: 8, h: 0.8,
|
||||
fontSize: 18, color: t.text, fontFace: 'Arial'
|
||||
});
|
||||
}
|
||||
s.addShape('rect', { x: 0.7, y: 4.2, w: 2.5, h: 0.03, fill: { color: t.secondary } });
|
||||
if (slide.content && slide.content.length > 0) {
|
||||
renderContent(s, slide.content, t, { startY: 4.5, maxY: 5.3 });
|
||||
}
|
||||
}
|
||||
|
||||
function renderEndingSlide(s, slide, t) {
|
||||
s.addShape('rect', { x: 1.5, y: 1.0, w: 7, h: 3.5, fill: { color: t.light } });
|
||||
s.addShape('rect', { x: 2.5, y: 1.3, w: 5, h: 0.04, fill: { color: t.accent } });
|
||||
s.addShape('rect', { x: 2.5, y: 4.2, w: 5, h: 0.04, fill: { color: t.accent } });
|
||||
s.addText(slide.title, {
|
||||
x: 1, y: 1.5, w: 8, h: 1.5,
|
||||
fontSize: 44, color: t.title, fontFace: 'Arial', bold: true,
|
||||
align: 'center', valign: 'middle'
|
||||
});
|
||||
if (slide.content && slide.content.length > 0) {
|
||||
var texts = [];
|
||||
for (var i = 0; i < slide.content.length; i++) {
|
||||
var ci = slide.content[i];
|
||||
if (ci.type === 'text') texts.push(ci.text);
|
||||
if (ci.type === 'list') texts = texts.concat(ci.items);
|
||||
if (ci.type === 'olist') texts = texts.concat(ci.items);
|
||||
}
|
||||
if (texts.length > 0) {
|
||||
s.addText(texts.join('\n'), {
|
||||
x: 2, y: 3.0, w: 6, h: 1.2,
|
||||
fontSize: 14, color: t.text, fontFace: 'Arial',
|
||||
align: 'center', valign: 'top'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderContentSlide(s, pres, slide, slideIndex, totalSlides, colors, t) {
|
||||
s.addShape('rect', { x: 0, y: 0.15, w: 10, h: 1.0, fill: { color: t.light } });
|
||||
s.addText(slide.title, {
|
||||
x: 0.5, y: 0.25, w: 9, h: 0.8,
|
||||
fontSize: 26, color: t.title, fontFace: 'Arial', bold: true, valign: 'middle'
|
||||
});
|
||||
|
||||
var chartData = null;
|
||||
var chartTableIdx = -1;
|
||||
|
||||
for (var ci = 0; ci < slide.content.length; ci++) {
|
||||
if (slide.content[ci].type === 'table') {
|
||||
var detected = detectChart(slide.content[ci], slide, ci);
|
||||
if (detected) {
|
||||
chartData = detected;
|
||||
chartTableIdx = ci;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chartData) {
|
||||
var remaining = slide.content.filter(function(_, idx) {
|
||||
return idx !== chartTableIdx && idx !== chartData.hintIndex;
|
||||
});
|
||||
var hasExtra = remaining.length > 0;
|
||||
|
||||
try {
|
||||
var isPie = chartData.type === 'pie';
|
||||
var chartW = hasExtra ? (isPie ? 5.5 : 6.0) : (isPie ? 6.5 : 9.0);
|
||||
|
||||
addChartToSlide(s, pres, chartData, colors, t, {
|
||||
x: 0.5, y: 1.4, w: chartW, h: 3.8
|
||||
});
|
||||
|
||||
if (hasExtra) {
|
||||
var sideX = chartW + 0.8;
|
||||
var sideW = 10 - sideX - 0.3;
|
||||
if (sideW > 1.5) {
|
||||
renderContent(s, remaining, t, { startY: 1.5, maxY: 5.0, x: sideX, w: sideW });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
renderContent(s, slide.content, t);
|
||||
}
|
||||
} else {
|
||||
renderContent(s, slide.content, t);
|
||||
}
|
||||
|
||||
s.addText((slideIndex + 1) + ' / ' + totalSlides, {
|
||||
x: 8.5, y: 5.3, w: 1.3, h: 0.25,
|
||||
fontSize: 9, color: t.secondary, fontFace: 'Arial', align: 'right'
|
||||
});
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 9. Main Generator
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
function createPPTX(markdownText, options) {
|
||||
options = options || {};
|
||||
var themeName = options.theme || 'ocean';
|
||||
var t = THEMES[themeName] || THEMES.ocean;
|
||||
var colors = (CHART_COLORS[themeName] || CHART_COLORS.ocean).slice();
|
||||
|
||||
var pres = new PptxGenJS();
|
||||
pres.layout = 'LAYOUT_16x9';
|
||||
|
||||
var slides = parse(markdownText);
|
||||
|
||||
if (slides.length === 0) {
|
||||
var emptySlide = pres.addSlide();
|
||||
emptySlide.background = { color: t.bg };
|
||||
emptySlide.addText('(empty content)', {
|
||||
x: 1, y: 2, w: 8, h: 1.5,
|
||||
fontSize: 24, color: t.text, fontFace: 'Arial', align: 'center', valign: 'middle'
|
||||
});
|
||||
return pres;
|
||||
}
|
||||
|
||||
for (var si = 0; si < slides.length; si++) {
|
||||
var slide = slides[si];
|
||||
var s = pres.addSlide();
|
||||
s.background = { color: t.bg };
|
||||
addDecorations(s, t);
|
||||
|
||||
if (slide.type === 'cover') {
|
||||
renderCoverSlide(s, slide, t);
|
||||
} else if (slide.type === 'ending') {
|
||||
renderEndingSlide(s, slide, t);
|
||||
s.addText((si + 1) + ' / ' + slides.length, {
|
||||
x: 8.5, y: 5.3, w: 1.3, h: 0.25,
|
||||
fontSize: 9, color: t.secondary, fontFace: 'Arial', align: 'right'
|
||||
});
|
||||
} else {
|
||||
renderContentSlide(s, pres, slide, si, slides.length, colors, t);
|
||||
}
|
||||
}
|
||||
|
||||
return pres;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 10. Module Export
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
module.exports = {
|
||||
createPPTX: createPPTX,
|
||||
parse: parse,
|
||||
THEMES: THEMES,
|
||||
CHART_COLORS: CHART_COLORS
|
||||
};
|
||||
Reference in New Issue
Block a user