dsh-ppt 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.en.md +4 -1
- package/README.md +4 -1
- package/lib/index.js +2 -2
- package/package.json +1 -1
- package/skills/dsh-ppt/SKILL.md +11 -3
- package/skills/dsh-ppt/scripts/deck-core.mjs +380 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **演讲者备注**:结构化 `slides` 的 `notes` 字段与 Markdown `<!-- 备注: ... -->` 注释都会成为该页备注;HTML 放映按 `S` 键(或右下角「备注」按钮)呼出备注面板;PPTX 生成原生 `notesSlide` + `notesMaster` 部件,PowerPoint 演示者视图直接可用。
|
|
6
|
+
- **新增 `quote` 金句版式**:`>` 引用块自动识别,末行 `—— 出处` 识别为署名;HTML 与 PPTX 双端实现。
|
|
7
|
+
- **新增 `table` 表格版式**:Markdown 表格(`| ... |`)自动识别,PPTX 用原生 `a:tbl` 图形帧(主题色表头 + 隔行着色),限 9 行 8 列。
|
|
8
|
+
- 版式集合扩为 `cover | section | bullets | statement | quote | table | closing`;结构化 `slides` 增加 `rows` 字段。
|
|
9
|
+
- 无表格数据的 `table` 页自动退化为要点页,避免空白版式。
|
|
10
|
+
- 新特性测试 8 例(解析/规范化/HTML/PPTX 部件/端到端)。
|
|
11
|
+
|
|
12
|
+
## 0.2.1
|
|
13
|
+
|
|
14
|
+
- 修复 PPTX zip 中央目录 local header 偏移恒为 0 导致 PowerPoint 提示修复的问题(issue #1/#3,社区 PR #2 同修法)。
|
|
15
|
+
|
|
3
16
|
## 0.2.0
|
|
4
17
|
|
|
5
18
|
- 新增 `defaultTheme` / `defaultLang` 配置,作为 `ppt_create` 未显式传参时的默认值。
|
package/README.en.md
CHANGED
|
@@ -14,6 +14,9 @@ DeepSeek Harness (DSH) presentation skill + tool plugin: turns a sentence, a par
|
|
|
14
14
|
| --- | --- |
|
|
15
15
|
| `ppt_create` tool | Markdown / structured slides → `*.html` + `*.pptx` + `*.json` |
|
|
16
16
|
| `ppt_themes` tool | Lists the 5 built-in themes and their best use cases |
|
|
17
|
+
| 7 layouts | cover / section / bullets / statement / **quote** / **table** / closing |
|
|
18
|
+
| Speaker notes | `<!-- note: ... -->` comments or the `notes` field: press `S` in the HTML player; native PPTX notes slides (presenter view) |
|
|
19
|
+
| Markdown extras | Tables (`\| ... \|`), blockquotes (`>`), and note comments are auto-detected into matching layouts |
|
|
17
20
|
| `dsh-ppt` skill | A complete six-step SOP registered into DSH |
|
|
18
21
|
| Standalone SKILL.md | Copy `skills/dsh-ppt/` into Claude Code / Cursor / Gemini CLI / Codex for cross-harness use |
|
|
19
22
|
| Visual engine | Reuses the hyperframes visual style library; HTML and PPTX share the same theme source |
|
|
@@ -26,7 +29,7 @@ Example:
|
|
|
26
29
|
|
|
27
30
|
## Compatibility
|
|
28
31
|
|
|
29
|
-
Verified against `@deepseek-ai/dsh@0.1.2-alpha.
|
|
32
|
+
Verified against `@deepseek-ai/dsh@0.1.2-alpha.3` on 2026-09-01. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
|
|
30
33
|
|
|
31
34
|
## Installation
|
|
32
35
|
|
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@ DSH(DeepSeek Harness)演示文稿技能 + 工具插件:把一句话、一
|
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| `ppt_create` 工具 | Markdown / 结构化 slides → `*.html` + `*.pptx` + `*.json` 三件套 |
|
|
18
18
|
| `ppt_themes` 工具 | 列出 5 套内置主题与适用场景 |
|
|
19
|
+
| 7 种页型 | 封面 / 章节 / 要点 / 核心观点 / **金句** / **表格** / 结束页 |
|
|
20
|
+
| 演讲者备注 | `<!-- 备注: ... -->` 或 `notes` 字段:HTML 按 `S` 呼出,PPTX 原生备注页(演示者视图) |
|
|
21
|
+
| Markdown 进阶 | 表格(`\| ... \|`)、引用金句(`>`)、备注注释自动识别成对应页型 |
|
|
19
22
|
| `dsh-ppt` 技能 | 完整 SOP 注册进 DSH:从一句话到成品 deck 的六步流水线 |
|
|
20
23
|
| 裸 SKILL.md | 把 `skills/dsh-ppt/` 复制到 Claude Code / Cursor / Gemini CLI / Codex 即可跨 harness 使用 |
|
|
21
24
|
| 视觉引擎 | 复用 hyperframes 视觉风格库,5 套主题 HTML 与 PPTX 同源同色 |
|
|
@@ -28,7 +31,7 @@ DSH(DeepSeek Harness)演示文稿技能 + 工具插件:把一句话、一
|
|
|
28
31
|
|
|
29
32
|
## 兼容性
|
|
30
33
|
|
|
31
|
-
在 `@deepseek-ai/dsh@0.1.2-alpha.
|
|
34
|
+
在 `@deepseek-ai/dsh@0.1.2-alpha.3` 上验证(2026-09-01 全量冒烟通过)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
|
|
32
35
|
|
|
33
36
|
## 安装
|
|
34
37
|
|
package/lib/index.js
CHANGED
|
@@ -157,10 +157,10 @@ export function apply(ctx, config = {}) {
|
|
|
157
157
|
description: 'Build a complete presentation deck from one sentence or a Markdown document and write three artifacts to outputDir: a standalone HTML web slideshow, an editable 16:9 PPTX, and a deck.json manifest. Five built-in visual themes are available (see ppt_themes). content is Markdown text (recommended); advanced callers may pass structured slides instead. 中文:把一句话或一篇 Markdown 文档生成完整演示文稿,写入 outputDir 三个文件:独立 HTML 网页放映、可编辑 16:9 PPTX、deck.json manifest;内置 5 套视觉主题。',
|
|
158
158
|
parameters: compileParameters({
|
|
159
159
|
title: { type: 'string', required: true, description: 'Deck title (used for the cover and file names).' },
|
|
160
|
-
content: { type: 'string', required: true, description: 'Markdown content: one sentence, a paragraph, or a full document. First # heading becomes the cover title; ## headings become slides; -/* lists become bullets. Required unless slides is provided.' },
|
|
160
|
+
content: { type: 'string', required: true, description: 'Markdown content: one sentence, a paragraph, or a full document. First # heading becomes the cover title; ## headings become slides; -/* lists become bullets; | ... | tables become table slides; > blockquotes become quote slides; <!-- 备注: ... --> comments become speaker notes. Required unless slides is provided.' },
|
|
161
161
|
theme: { type: 'string', description: 'Visual theme id: swiss / velvet / data / soft / bold. Default data. See ppt_themes.' },
|
|
162
162
|
lang: { type: 'string', description: 'UI language of the generated player: zh (default), en, or bilingual. Content language is whatever you write.' },
|
|
163
|
-
slides: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Optional structured slides: [{ layout: cover|section|bullets|statement|closing, title, subtitle, kicker, bullets: [] }]. Use this for precise control instead of content.' },
|
|
163
|
+
slides: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Optional structured slides: [{ layout: cover|section|bullets|statement|quote|table|closing, title, subtitle, kicker, bullets: [], rows: [][] (for table), notes: "speaker notes" }]. Use this for precise control instead of content.' },
|
|
164
164
|
outputDir: { type: 'string', description: 'Directory to write the files into. Default: session working directory (or the plugin outputDir config).' },
|
|
165
165
|
fileName: { type: 'string', description: 'Base file name for the three artifacts. Default: sanitized deck title.' },
|
|
166
166
|
}),
|
package/package.json
CHANGED
package/skills/dsh-ppt/SKILL.md
CHANGED
|
@@ -25,7 +25,7 @@ allowed-tools: "Bash, Read, Write, Edit"
|
|
|
25
25
|
- 跨 harness:`node <skill-dir>/scripts/build-deck.mjs --title "标题" --content deck.md --theme data --lang zh --out dist`
|
|
26
26
|
- 输出三件套:`*.html`(双击即放映)、`*.pptx`(可编辑)、`*.json`(manifest)。
|
|
27
27
|
5. **Verify 校验**:确认 HTML 页数与大纲一致、标题/要点无截断;PPTX 文件头为 `PK`(zip)。DSH 内用文件工具读取确认;跨 harness 用 `unzip -l deck.pptx | grep presentation.xml` 或 PowerShell `Expand-Archive` 抽查。
|
|
28
|
-
6. **Deliver 交付**:给用户三个绝对路径,并说明:HTML 浏览器直接打开(方向键翻页、F 全屏、G 总览、P 打印/另存 PDF);PPTX 用 PowerPoint / WPS / Keynote
|
|
28
|
+
6. **Deliver 交付**:给用户三个绝对路径,并说明:HTML 浏览器直接打开(方向键翻页、F 全屏、G 总览、S 演讲者备注、P 打印/另存 PDF);PPTX 用 PowerPoint / WPS / Keynote 打开(备注在演示者视图可见)。
|
|
29
29
|
|
|
30
30
|
## 内容写作规则
|
|
31
31
|
|
|
@@ -35,6 +35,12 @@ allowed-tools: "Bash, Read, Write, Edit"
|
|
|
35
35
|
- 中英双语:`lang` 只决定界面文字(页码/主题标签/结束页),**内容语言由你撰写**。要求双语时,优先每页中文标题 + 英文副标题,或直接生成两份 deck(`--lang zh` 与 `--lang en`)。
|
|
36
36
|
- 更多规则见 `references/copywriting.md`。
|
|
37
37
|
|
|
38
|
+
## Markdown 进阶语法(自动识别)
|
|
39
|
+
|
|
40
|
+
- **表格**:`| 列 | 列 |` 连续行(第二行 `| --- | --- |` 分隔线)→ 独立 `table` 页(主题色表头,限 9 行 8 列)。数据对比优先用表格,别堆要点。
|
|
41
|
+
- **金句**:`>` 引用块 → 独立 `quote` 页;最后一行写 `—— 出处` 会成为署名。
|
|
42
|
+
- **演讲者备注**:`<!-- 备注: 这页要讲的内容 -->`(或 `<!-- note: ... -->`)附着到该页。备注不进正文:HTML 放映按 `S` 呼出备注面板,PPTX 写入原生备注(演示者视图可见)。为汇报/演讲类 deck 主动写备注(口径、数据出处、应答预案)。
|
|
43
|
+
|
|
38
44
|
## 主题选择
|
|
39
45
|
|
|
40
46
|
内置 5 套主题(源自 hyperframes 视觉风格库):
|
|
@@ -59,15 +65,17 @@ allowed-tools: "Bash, Read, Write, Edit"
|
|
|
59
65
|
|
|
60
66
|
```json
|
|
61
67
|
[
|
|
62
|
-
{ "layout": "cover", "title": "标题", "subtitle": "副标题", "kicker": "开场" },
|
|
68
|
+
{ "layout": "cover", "title": "标题", "subtitle": "副标题", "kicker": "开场", "notes": "开场先自我介绍 30 秒" },
|
|
63
69
|
{ "layout": "section", "kicker": "01", "title": "背景" },
|
|
64
70
|
{ "layout": "bullets", "title": "三个论点", "bullets": ["论点一", "论点二", "论点三"] },
|
|
71
|
+
{ "layout": "table", "title": "业绩对比", "rows": [["大区", "营收"], ["华东", "1.2 亿"]], "notes": "强调华东增速" },
|
|
72
|
+
{ "layout": "quote", "title": "慢就是快。", "subtitle": "创始人" },
|
|
65
73
|
{ "layout": "statement", "title": "核心观点一句话", "subtitle": "支撑说明" },
|
|
66
74
|
{ "layout": "closing", "title": "谢谢", "subtitle": "行动号召" }
|
|
67
75
|
]
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
`layout` 仅限 `cover | section | bullets | statement | closing
|
|
78
|
+
`layout` 仅限 `cover | section | bullets | statement | quote | table | closing`;每页可选 `notes`(演讲者备注),`table` 页用 `rows`(首行为表头)。Markdown 输入不够精确时,用结构化 slides 重写。
|
|
71
79
|
|
|
72
80
|
## 质量门禁(交付前逐项确认)
|
|
73
81
|
|
|
@@ -18,7 +18,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'
|
|
|
18
18
|
import { resolve as resolvePath } from 'node:path'
|
|
19
19
|
import { deflateRawSync } from 'node:zlib'
|
|
20
20
|
|
|
21
|
-
export const DECK_VERSION = '0.
|
|
21
|
+
export const DECK_VERSION = '0.2.0'
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// 主题
|
|
@@ -133,10 +133,14 @@ export const LANGUAGES = {
|
|
|
133
133
|
slide: '第',
|
|
134
134
|
of: '/ 共',
|
|
135
135
|
theme: '主题',
|
|
136
|
-
help: '← → 翻页 · F 全屏 · G 总览 · P 打印',
|
|
136
|
+
help: '← → 翻页 · F 全屏 · G 总览 · S 备注 · P 打印',
|
|
137
137
|
coverKicker: '开场',
|
|
138
138
|
pointKicker: '要点',
|
|
139
139
|
statementKicker: '核心观点',
|
|
140
|
+
quoteKicker: '金句',
|
|
141
|
+
tableKicker: '数据',
|
|
142
|
+
notesLabel: '演讲者备注',
|
|
143
|
+
notesToggle: '备注',
|
|
140
144
|
closingTitle: '谢谢',
|
|
141
145
|
closingSubtitle: '讨论与问答',
|
|
142
146
|
generatedBy: '由 dsh-ppt 生成',
|
|
@@ -149,10 +153,14 @@ export const LANGUAGES = {
|
|
|
149
153
|
slide: 'Slide',
|
|
150
154
|
of: '/',
|
|
151
155
|
theme: 'Theme',
|
|
152
|
-
help: '← → navigate · F fullscreen · G overview · P print',
|
|
156
|
+
help: '← → navigate · F fullscreen · G overview · S notes · P print',
|
|
153
157
|
coverKicker: 'Opening',
|
|
154
158
|
pointKicker: 'Key point',
|
|
155
159
|
statementKicker: 'Core idea',
|
|
160
|
+
quoteKicker: 'Quote',
|
|
161
|
+
tableKicker: 'Data',
|
|
162
|
+
notesLabel: 'Speaker notes',
|
|
163
|
+
notesToggle: 'Notes',
|
|
156
164
|
closingTitle: 'Thank You',
|
|
157
165
|
closingSubtitle: 'Discussion & Q&A',
|
|
158
166
|
generatedBy: 'Generated with dsh-ppt',
|
|
@@ -165,10 +173,14 @@ export const LANGUAGES = {
|
|
|
165
173
|
slide: '第',
|
|
166
174
|
of: '/ 共',
|
|
167
175
|
theme: '主题 · Theme',
|
|
168
|
-
help: '← → 翻页 · F 全屏 · G 总览 · P 打印',
|
|
176
|
+
help: '← → 翻页 · F 全屏 · G 总览 · S 备注 · P 打印',
|
|
169
177
|
coverKicker: '开场 · Opening',
|
|
170
178
|
pointKicker: '要点 · Key point',
|
|
171
179
|
statementKicker: '核心观点 · Core Idea',
|
|
180
|
+
quoteKicker: '金句 · Quote',
|
|
181
|
+
tableKicker: '数据 · Data',
|
|
182
|
+
notesLabel: '演讲者备注 · Speaker notes',
|
|
183
|
+
notesToggle: '备注 · Notes',
|
|
172
184
|
closingTitle: '谢谢 · Thank You',
|
|
173
185
|
closingSubtitle: '讨论与问答 · Q&A',
|
|
174
186
|
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
@@ -285,7 +297,49 @@ function chunk(items, size) {
|
|
|
285
297
|
// ---------------------------------------------------------------------------
|
|
286
298
|
|
|
287
299
|
function createSection(heading = '', level = 0, coverOnly = false) {
|
|
288
|
-
return { heading, level, coverOnly, bullets: [], paragraphs: [] }
|
|
300
|
+
return { heading, level, coverOnly, bullets: [], paragraphs: [], quote: [], table: null, notes: [] }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function splitTableRow(line) {
|
|
304
|
+
const trimmed = line.trim()
|
|
305
|
+
const match = /^\|(.+)\|$/.exec(trimmed)
|
|
306
|
+
if (match === null) return null
|
|
307
|
+
return match[1].split('|').map((cell) => cell.trim())
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isTableSeparator(cells) {
|
|
311
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{2,}:?$/.test(cell) || /^:?-+:?$/.test(cell))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const NOTES_COMMENT = /^<!--\s*(?:notes?|备注)\s*[::]\s*([\s\S]*?)\s*-->$/
|
|
315
|
+
|
|
316
|
+
function quoteSlideFrom(section, ui) {
|
|
317
|
+
const lines = [...section.quote]
|
|
318
|
+
let attribution = ''
|
|
319
|
+
const attrMatch = /^(?:——|—|--|-)\s*(.+)$/.exec(lines[lines.length - 1] ?? '')
|
|
320
|
+
if (attrMatch !== null && lines.length > 1) {
|
|
321
|
+
attribution = attrMatch[1]
|
|
322
|
+
lines.pop()
|
|
323
|
+
}
|
|
324
|
+
return { layout: 'quote', kicker: ui.quoteKicker, title: truncate(lines.join(' '), 220), subtitle: attribution }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function tableSlideFrom(section, ui) {
|
|
328
|
+
const table = section.table
|
|
329
|
+
const rows = [table.header, ...table.rows]
|
|
330
|
+
.slice(0, 9)
|
|
331
|
+
.map((row) => row.slice(0, 8).map((cell) => truncate(cell, 60)))
|
|
332
|
+
const heading = section.heading || ui.tableKicker
|
|
333
|
+
return { layout: 'table', kicker: heading, title: heading, rows }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** 把一个 section 里的金句/表格拆成独立页;备注附着到第一张产出页。 */
|
|
337
|
+
function specialSlidesFor(section, ui) {
|
|
338
|
+
const out = []
|
|
339
|
+
if (section.quote.length > 0) out.push(quoteSlideFrom(section, ui))
|
|
340
|
+
if (section.table !== null) out.push(tableSlideFrom(section, ui))
|
|
341
|
+
if (out.length > 0 && section.notes.length > 0) out[0].notes = section.notes.join('\n')
|
|
342
|
+
return out
|
|
289
343
|
}
|
|
290
344
|
|
|
291
345
|
export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
@@ -347,6 +401,38 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
347
401
|
continue
|
|
348
402
|
}
|
|
349
403
|
|
|
404
|
+
// 演讲者备注:`<!-- 备注: ... -->` / `<!-- note: ... -->` 附到当前页
|
|
405
|
+
const notesMatch = NOTES_COMMENT.exec(line)
|
|
406
|
+
if (notesMatch !== null && notesMatch[1].trim() !== '') {
|
|
407
|
+
if (current === null) current = createSection()
|
|
408
|
+
current.notes.push(notesMatch[1].trim())
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Markdown 表格:连续的 | ... | 行,第二行是分隔线
|
|
413
|
+
const tableCells = splitTableRow(line)
|
|
414
|
+
if (tableCells !== null) {
|
|
415
|
+
if (current === null) current = createSection()
|
|
416
|
+
if (isTableSeparator(tableCells)) continue
|
|
417
|
+
if (current.table === null) {
|
|
418
|
+
current.table = { header: tableCells.map((cell) => stripInlineMarkdown(cell)), rows: [] }
|
|
419
|
+
} else if (current.table.rows.length < 12) {
|
|
420
|
+
current.table.rows.push(tableCells.map((cell) => stripInlineMarkdown(cell)))
|
|
421
|
+
}
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 金句:`>` 引用块(最后一行 `—— 出处` 识别为署名)
|
|
426
|
+
const quoteMatch = /^>\s?(.*)$/.exec(line)
|
|
427
|
+
if (quoteMatch !== null) {
|
|
428
|
+
const quoted = stripInlineMarkdown(quoteMatch[1])
|
|
429
|
+
if (quoted !== '') {
|
|
430
|
+
if (current === null) current = createSection()
|
|
431
|
+
current.quote.push(quoted)
|
|
432
|
+
}
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
|
|
350
436
|
const paragraph = stripInlineMarkdown(line)
|
|
351
437
|
if (paragraph === '') continue
|
|
352
438
|
if (current === null) current = createSection()
|
|
@@ -404,24 +490,31 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
404
490
|
}))
|
|
405
491
|
const firstSentence = ordered[0]?.sentences[0] ?? ''
|
|
406
492
|
if (coverSubtitle === '') coverSubtitle = truncate(firstSentence, 180)
|
|
407
|
-
const
|
|
493
|
+
const bodySlides = []
|
|
494
|
+
let pointIndex = 0
|
|
495
|
+
ordered.forEach((group, groupIndex) => {
|
|
496
|
+
const specials = specialSlidesFor(group.section, ui)
|
|
497
|
+
bodySlides.push(...specials)
|
|
408
498
|
const sentences = (groupIndex === 0 && !coverSubtitleConsumed) ? group.sentences.slice(1) : group.sentences
|
|
409
|
-
|
|
499
|
+
for (const points of chunk(sentences, 5)) {
|
|
500
|
+
pointIndex += 1
|
|
501
|
+
const slide = {
|
|
502
|
+
layout: 'bullets',
|
|
503
|
+
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
504
|
+
title: truncate(points[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
505
|
+
bullets: points,
|
|
506
|
+
}
|
|
507
|
+
if (specials.length === 0 && group.section.notes.length > 0) slide.notes = group.section.notes.join('\n')
|
|
508
|
+
bodySlides.push(slide)
|
|
509
|
+
}
|
|
410
510
|
})
|
|
411
511
|
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
412
|
-
if (
|
|
512
|
+
if (bodySlides.length === 0) {
|
|
413
513
|
if (coverSubtitle !== '') {
|
|
414
514
|
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle || 'Untitled' })
|
|
415
515
|
}
|
|
416
516
|
} else {
|
|
417
|
-
|
|
418
|
-
slides.push({
|
|
419
|
-
layout: 'bullets',
|
|
420
|
-
kicker: ui.pointKicker + ' ' + (index + 1),
|
|
421
|
-
title: truncate(points[0], 40) || ui.pointKicker + ' ' + (index + 1),
|
|
422
|
-
bullets: points,
|
|
423
|
-
})
|
|
424
|
-
})
|
|
517
|
+
slides.push(...bodySlides)
|
|
425
518
|
}
|
|
426
519
|
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
427
520
|
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
@@ -430,18 +523,23 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
430
523
|
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
431
524
|
let pointIndex = 0
|
|
432
525
|
for (const section of bodySections) {
|
|
526
|
+
const notes = section.notes.join('\n')
|
|
527
|
+
const specials = specialSlidesFor(section, ui)
|
|
528
|
+
slides.push(...specials)
|
|
433
529
|
if (section.heading === '') {
|
|
434
530
|
const points = [...section.bullets, ...section.paragraphs]
|
|
435
531
|
.flatMap((part) => splitSentences(part))
|
|
436
532
|
.slice(0, 8)
|
|
437
533
|
if (points.length > 0) {
|
|
438
534
|
pointIndex += 1
|
|
439
|
-
|
|
535
|
+
const slide = {
|
|
440
536
|
layout: 'bullets',
|
|
441
537
|
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
442
538
|
title: truncate(points[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
443
539
|
bullets: points,
|
|
444
|
-
}
|
|
540
|
+
}
|
|
541
|
+
if (specials.length === 0 && notes !== '') slide.notes = notes
|
|
542
|
+
slides.push(slide)
|
|
445
543
|
}
|
|
446
544
|
continue
|
|
447
545
|
}
|
|
@@ -450,16 +548,20 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
450
548
|
...section.paragraphs.flatMap((part) => splitSentences(part)),
|
|
451
549
|
].slice(0, 8)
|
|
452
550
|
if (points.length > 0) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
slides.push(
|
|
551
|
+
const slide = { layout: 'bullets', kicker: section.heading, title: section.heading, bullets: points }
|
|
552
|
+
if (specials.length === 0 && notes !== '') slide.notes = notes
|
|
553
|
+
slides.push(slide)
|
|
554
|
+
} else if (specials.length === 0) {
|
|
555
|
+
const slide = { layout: 'section', kicker: section.heading, title: section.heading }
|
|
556
|
+
if (notes !== '') slide.notes = notes
|
|
557
|
+
slides.push(slide)
|
|
456
558
|
}
|
|
457
559
|
}
|
|
458
560
|
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
459
561
|
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
460
562
|
}
|
|
461
563
|
|
|
462
|
-
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing'])
|
|
564
|
+
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing', 'quote', 'table'])
|
|
463
565
|
|
|
464
566
|
function normalizeSlide(raw, index) {
|
|
465
567
|
const source = (raw !== null && typeof raw === 'object') ? raw : {}
|
|
@@ -468,6 +570,8 @@ function normalizeSlide(raw, index) {
|
|
|
468
570
|
const subtitle = stripInlineMarkdown(source.subtitle ?? '')
|
|
469
571
|
const kicker = stripInlineMarkdown(source.kicker ?? '')
|
|
470
572
|
const text = stripInlineMarkdown(source.text ?? '')
|
|
573
|
+
// 备注保留换行(放映面板与备注页都按原文显示),只做转义,不做 Markdown 清洗
|
|
574
|
+
const notes = String(source.notes ?? '').trim()
|
|
471
575
|
let bullets = []
|
|
472
576
|
if (Array.isArray(source.bullets)) {
|
|
473
577
|
bullets = source.bullets.map((item) => stripInlineMarkdown(String(item))).filter(Boolean)
|
|
@@ -475,9 +579,29 @@ function normalizeSlide(raw, index) {
|
|
|
475
579
|
bullets = splitSentences(source.bullets)
|
|
476
580
|
}
|
|
477
581
|
if (layout === 'statement' && title === '' && text !== '') {
|
|
478
|
-
return { layout, kicker, title: text, subtitle: subtitle || '', bullets }
|
|
582
|
+
return { layout, kicker, title: text, subtitle: subtitle || '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
583
|
+
}
|
|
584
|
+
if (layout === 'quote') {
|
|
585
|
+
const quoteText = title !== '' ? title : text
|
|
586
|
+
if (quoteText === '') return { layout: 'statement', kicker, title: subtitle, subtitle: '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
587
|
+
return { layout, kicker, title: truncate(quoteText, 220), subtitle, ...(notes !== '' ? { notes } : {}) }
|
|
588
|
+
}
|
|
589
|
+
if (layout === 'table') {
|
|
590
|
+
let rows = []
|
|
591
|
+
if (Array.isArray(source.rows)) {
|
|
592
|
+
rows = source.rows
|
|
593
|
+
.map((row) => (Array.isArray(row) ? row : [row]).map((cell) => stripInlineMarkdown(String(cell))))
|
|
594
|
+
.filter((row) => row.some((cell) => cell !== ''))
|
|
595
|
+
.slice(0, 9)
|
|
596
|
+
.map((row) => row.slice(0, 8).map((cell) => truncate(cell, 60)))
|
|
597
|
+
}
|
|
598
|
+
if (rows.length === 0) {
|
|
599
|
+
// 没有表格数据的 table 页退化成要点页,避免产出空白版式
|
|
600
|
+
return { layout: 'bullets', kicker, title, subtitle, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
601
|
+
}
|
|
602
|
+
return { layout, kicker: kicker || title, title, rows, ...(notes !== '' ? { notes } : {}) }
|
|
479
603
|
}
|
|
480
|
-
return { layout, kicker, title, subtitle, text, bullets }
|
|
604
|
+
return { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
481
605
|
}
|
|
482
606
|
|
|
483
607
|
export function normalizeSlides(rawSlides, maxSlides = 60) {
|
|
@@ -647,6 +771,28 @@ h2{font-size:clamp(34px,5vw,82px);max-width:20ch}
|
|
|
647
771
|
.closing h1,.closing .statement-title{font-size:clamp(52px,9vw,148px)}
|
|
648
772
|
.closing .subtitle{color:var(--accent);font-weight:700}
|
|
649
773
|
.cover h1{font-weight:900}
|
|
774
|
+
.quote-mark{
|
|
775
|
+
font-family:var(--font-heading);font-size:clamp(90px,13vw,210px);line-height:.62;
|
|
776
|
+
color:var(--accent);margin-bottom:clamp(14px,2vw,30px);
|
|
777
|
+
}
|
|
778
|
+
.quote-text{
|
|
779
|
+
font-family:var(--font-heading);font-size:clamp(26px,4.2vw,62px);line-height:1.28;
|
|
780
|
+
font-weight:700;max-width:22ch;margin:0;
|
|
781
|
+
}
|
|
782
|
+
.quote-attr{color:var(--muted);margin-top:clamp(18px,2.6vw,34px);font-size:clamp(15px,1.8vw,26px);letter-spacing:.04em}
|
|
783
|
+
table.deck-table{
|
|
784
|
+
border-collapse:collapse;margin-top:clamp(20px,3vw,42px);width:100%;max-width:1120px;
|
|
785
|
+
font-size:clamp(13px,1.55vw,23px);
|
|
786
|
+
}
|
|
787
|
+
.deck-table th{
|
|
788
|
+
background:var(--accent);color:var(--bg);text-align:left;font-weight:700;
|
|
789
|
+
padding:.6em .9em;letter-spacing:.02em;
|
|
790
|
+
}
|
|
791
|
+
.deck-table td{
|
|
792
|
+
padding:.55em .9em;border-bottom:1px solid color-mix(in srgb, var(--muted) 34%, transparent);
|
|
793
|
+
color:var(--fg);
|
|
794
|
+
}
|
|
795
|
+
.deck-table tbody tr:nth-child(even){background:color-mix(in srgb, var(--panel) 62%, transparent)}
|
|
650
796
|
#progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);z-index:30;transition:width .25s}
|
|
651
797
|
#hud{
|
|
652
798
|
position:fixed;right:22px;bottom:18px;z-index:30;display:flex;gap:14px;align-items:center;
|
|
@@ -658,16 +804,29 @@ h2{font-size:clamp(34px,5vw,82px);max-width:20ch}
|
|
|
658
804
|
padding:7px 13px;font:inherit;cursor:pointer;
|
|
659
805
|
}
|
|
660
806
|
#hud button:hover{border-color:var(--accent);color:var(--accent)}
|
|
807
|
+
#notes-panel{
|
|
808
|
+
position:fixed;left:0;right:0;bottom:0;z-index:40;display:none;
|
|
809
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);
|
|
810
|
+
border-top:2px solid var(--accent);
|
|
811
|
+
padding:16px clamp(22px,4vw,64px) 20px;max-height:38vh;overflow:auto;
|
|
812
|
+
}
|
|
813
|
+
body.notes-open #notes-panel.has-notes{display:block}
|
|
814
|
+
#notes-panel .notes-label{
|
|
815
|
+
color:var(--accent);font-weight:700;letter-spacing:.14em;text-transform:uppercase;
|
|
816
|
+
font-size:12px;margin-bottom:8px;
|
|
817
|
+
}
|
|
818
|
+
#notes-text{white-space:pre-wrap;line-height:1.65;font-size:15px;color:var(--fg)}
|
|
661
819
|
body.overview .slide{display:flex !important;position:relative;inset:auto;width:100%;height:100vh}
|
|
662
820
|
body.overview{overflow:auto}
|
|
663
821
|
body.overview #progress,body.overview #hud{position:fixed}
|
|
822
|
+
body.overview #notes-panel{display:none !important}
|
|
664
823
|
@media (max-width:640px){
|
|
665
824
|
#hud{right:12px;bottom:10px;gap:8px;font-size:11px}
|
|
666
825
|
}
|
|
667
826
|
@media print{
|
|
668
827
|
html,body{height:auto;overflow:visible;background:#fff}
|
|
669
828
|
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
670
|
-
#progress,#hud{display:none !important}
|
|
829
|
+
#progress,#hud,#notes-panel{display:none !important}
|
|
671
830
|
}
|
|
672
831
|
</style>
|
|
673
832
|
</head>
|
|
@@ -677,23 +836,38 @@ ${slides}
|
|
|
677
836
|
<div id="hud" aria-live="polite">
|
|
678
837
|
<span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
|
|
679
838
|
<span id="theme-label">${ui.theme} · ${escapeHtml(themeLabel)}</span>
|
|
839
|
+
<button id="notes-toggle" title="S">${escapeHtml(ui.notesToggle)}</button>
|
|
680
840
|
<button id="fullscreen" title="F">⛶</button>
|
|
681
841
|
</div>
|
|
842
|
+
<div id="notes-panel" aria-live="polite">
|
|
843
|
+
<div class="notes-label">${escapeHtml(ui.notesLabel)}</div>
|
|
844
|
+
<div id="notes-text"></div>
|
|
845
|
+
</div>
|
|
682
846
|
<script>
|
|
683
847
|
(() => {
|
|
684
848
|
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
685
849
|
const counter = document.getElementById('counter');
|
|
686
850
|
const progress = document.getElementById('progress');
|
|
687
851
|
const fullscreenBtn = document.getElementById('fullscreen');
|
|
852
|
+
const notesPanel = document.getElementById('notes-panel');
|
|
853
|
+
const notesText = document.getElementById('notes-text');
|
|
854
|
+
const notesBtn = document.getElementById('notes-toggle');
|
|
688
855
|
let index = 0;
|
|
689
856
|
const total = slides.length;
|
|
690
857
|
const ui = ${JSON.stringify(ui).replace(/</g, '\\u003c')};
|
|
691
858
|
|
|
859
|
+
function refreshNotes() {
|
|
860
|
+
const notes = slides[index]?.dataset.notes || '';
|
|
861
|
+
notesPanel.classList.toggle('has-notes', notes !== '');
|
|
862
|
+
notesText.textContent = notes;
|
|
863
|
+
}
|
|
864
|
+
|
|
692
865
|
function go(next) {
|
|
693
866
|
index = (next + total) % total;
|
|
694
867
|
slides.forEach((slide, i) => slide.classList.toggle('is-active', i === index));
|
|
695
868
|
counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
|
|
696
869
|
progress.style.width = ((index + 1) / total * 100) + '%';
|
|
870
|
+
refreshNotes();
|
|
697
871
|
document.title = (index + 1) + ' / ' + total + ' · ' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
698
872
|
try { history.replaceState?.(null, '', '#slide-' + (index + 1)); } catch { /* file:// 下个别浏览器可能拒绝 */ }
|
|
699
873
|
}
|
|
@@ -713,6 +887,8 @@ ${slides}
|
|
|
713
887
|
} else if (event.key.toLowerCase() === 'g') {
|
|
714
888
|
document.body.classList.toggle('overview');
|
|
715
889
|
go(index);
|
|
890
|
+
} else if (event.key.toLowerCase() === 's') {
|
|
891
|
+
document.body.classList.toggle('notes-open');
|
|
716
892
|
} else if (event.key.toLowerCase() === 'p') {
|
|
717
893
|
window.print();
|
|
718
894
|
}
|
|
@@ -738,6 +914,10 @@ ${slides}
|
|
|
738
914
|
else document.exitFullscreen?.();
|
|
739
915
|
});
|
|
740
916
|
|
|
917
|
+
notesBtn.addEventListener('click', () => {
|
|
918
|
+
document.body.classList.toggle('notes-open');
|
|
919
|
+
});
|
|
920
|
+
|
|
741
921
|
const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
|
|
742
922
|
go(Number.isInteger(start) ? start - 1 : 0);
|
|
743
923
|
})();
|
|
@@ -771,6 +951,25 @@ function renderHtmlSlide(slide, index, ui, langId) {
|
|
|
771
951
|
'<div class="statement-title">' + escapeHtml(title || slide.text || '') + '</div>' +
|
|
772
952
|
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
773
953
|
break
|
|
954
|
+
case 'quote':
|
|
955
|
+
inner = '<div class="kicker">' + escapeHtml(kicker || ui.quoteKicker) + '</div>' +
|
|
956
|
+
'<div class="quote-mark" aria-hidden="true">\u201C</div>' +
|
|
957
|
+
'<blockquote class="quote-text">' + escapeHtml(title || slide.text || '') + '</blockquote>' +
|
|
958
|
+
(subtitle !== '' ? '<div class="quote-attr">\u2014\u2014 ' + escapeHtml(subtitle) + '</div>' : '')
|
|
959
|
+
break
|
|
960
|
+
case 'table': {
|
|
961
|
+
const rows = Array.isArray(slide.rows) ? slide.rows : []
|
|
962
|
+
const head = rows[0] ?? []
|
|
963
|
+
const body = rows.slice(1)
|
|
964
|
+
inner = '<div class="kicker">' + escapeHtml(kicker || ui.tableKicker) + '</div>' +
|
|
965
|
+
(title !== '' && title !== kicker ? '<h2>' + escapeHtml(title) + '</h2>' : '') +
|
|
966
|
+
'<table class="deck-table"><thead><tr>' +
|
|
967
|
+
head.map((cell) => '<th>' + escapeHtml(cell) + '</th>').join('') +
|
|
968
|
+
'</tr></thead><tbody>' +
|
|
969
|
+
body.map((row) => '<tr>' + row.map((cell) => '<td>' + escapeHtml(cell) + '</td>').join('') + '</tr>').join('') +
|
|
970
|
+
'</tbody></table>'
|
|
971
|
+
break
|
|
972
|
+
}
|
|
774
973
|
case 'closing':
|
|
775
974
|
inner = '<h1>' + escapeHtml(title || ui.closingTitle) + '</h1>' +
|
|
776
975
|
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
@@ -783,7 +982,10 @@ function renderHtmlSlide(slide, index, ui, langId) {
|
|
|
783
982
|
'<ul>' + bullets.map((bullet) => '<li>' + escapeHtml(bullet) + '</li>').join('') + '</ul>'
|
|
784
983
|
break
|
|
785
984
|
}
|
|
786
|
-
|
|
985
|
+
const notesAttr = typeof slide.notes === 'string' && slide.notes.trim() !== ''
|
|
986
|
+
? ' data-notes="' + escapeHtml(slide.notes) + '"'
|
|
987
|
+
: ''
|
|
988
|
+
return '<section class="slide slide--' + escapeHtml(slide.layout || 'bullets') + '" data-index="' + number + '"' + notesAttr + '>' +
|
|
787
989
|
'<div class="slide-inner">' + inner + '</div></section>'
|
|
788
990
|
}
|
|
789
991
|
|
|
@@ -862,6 +1064,42 @@ function accentBarXml(id, name, box, color) {
|
|
|
862
1064
|
'<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>'
|
|
863
1065
|
}
|
|
864
1066
|
|
|
1067
|
+
/** 原生 OOXML 表格(graphicFrame + a:tbl):首行主题色表头,隔行面板色。 */
|
|
1068
|
+
function tableGraphicXml(id, name, box, rows, theme) {
|
|
1069
|
+
const p = theme.palette
|
|
1070
|
+
const font = pptxFontName(theme.fonts.body)
|
|
1071
|
+
const safeRows = rows.length > 0 ? rows : [['']]
|
|
1072
|
+
const cols = Math.max(1, safeRows[0].length)
|
|
1073
|
+
const colW = Math.floor(box.w / cols)
|
|
1074
|
+
const grid = Array.from({ length: cols }, () => '<a:gridCol w="' + colW + '"/>').join('')
|
|
1075
|
+
const rowH = 420000
|
|
1076
|
+
const cellXml = (text, fill, color, bold) =>
|
|
1077
|
+
'<a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="l"><a:buNone/></a:pPr>' +
|
|
1078
|
+
'<a:r><a:rPr lang="zh-CN" sz="1400" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1079
|
+
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1080
|
+
'<a:latin typeface="' + escapeXml(font) + '"/><a:ea typeface="' + escapeXml(font) + '"/></a:rPr>' +
|
|
1081
|
+
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p></a:txBody>' +
|
|
1082
|
+
'<a:tcPr marL="91440" marR="91440" anchor="ctr">' +
|
|
1083
|
+
(fill !== '' ? '<a:solidFill><a:srgbClr val="' + hex(fill) + '"/></a:solidFill>' : '<a:noFill/>') +
|
|
1084
|
+
'</a:tcPr></a:tc>'
|
|
1085
|
+
const trs = safeRows.map((row, rowIndex) => {
|
|
1086
|
+
const padded = Array.from({ length: cols }, (_, i) => row[i] ?? '')
|
|
1087
|
+
const cells = rowIndex === 0
|
|
1088
|
+
? padded.map((c) => cellXml(c, p.accent, p.bg, true)).join('')
|
|
1089
|
+
: padded.map((c) => cellXml(c, rowIndex % 2 === 1 ? p.panel : '', p.fg, false)).join('')
|
|
1090
|
+
return '<a:tr h="' + rowH + '">' + cells + '</a:tr>'
|
|
1091
|
+
}).join('')
|
|
1092
|
+
const height = Math.max(1, safeRows.length) * rowH
|
|
1093
|
+
return '<p:graphicFrame>' +
|
|
1094
|
+
'<p:nvGraphicFramePr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/>' +
|
|
1095
|
+
'<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>' +
|
|
1096
|
+
'<p:xfrm><a:off x="' + box.x + '" y="' + box.y + '"/><a:ext cx="' + box.w + '" cy="' + height + '"/></p:xfrm>' +
|
|
1097
|
+
'<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">' +
|
|
1098
|
+
'<a:tbl><a:tblPr firstRow="1" bandRow="1"/>' +
|
|
1099
|
+
'<a:tblGrid>' + grid + '</a:tblGrid>' + trs + '</a:tbl>' +
|
|
1100
|
+
'</a:graphicData></a:graphic></p:graphicFrame>'
|
|
1101
|
+
}
|
|
1102
|
+
|
|
865
1103
|
function slideShapeList(slide, index, total, theme, langId) {
|
|
866
1104
|
const p = theme.palette
|
|
867
1105
|
const font = pptxFontName(theme.fonts.heading)
|
|
@@ -914,6 +1152,31 @@ function slideShapeList(slide, index, total, theme, langId) {
|
|
|
914
1152
|
paragraphXml(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
915
1153
|
]))
|
|
916
1154
|
}
|
|
1155
|
+
} else if (slide.layout === 'quote') {
|
|
1156
|
+
shapes.push(textShapeXml(idBase + 1, 'Quote mark', { x: 900000, y: 1050000, w: 1700000, h: 1900000 }, [
|
|
1157
|
+
paragraphXml('\u201C', { size: 9600, color: p.accent, bold: true, font }),
|
|
1158
|
+
]))
|
|
1159
|
+
shapes.push(textShapeXml(idBase + 2, 'Quote', { x: 1050000, y: 2550000, w: 10100000, h: 2600000 }, [
|
|
1160
|
+
paragraphXml(title || slide.text || '', { size: 3000, color: p.fg, bold: true, font }),
|
|
1161
|
+
]))
|
|
1162
|
+
if (subtitle !== '') {
|
|
1163
|
+
shapes.push(textShapeXml(idBase + 3, 'Attribution', { x: 1070000, y: 5300000, w: 9000000, h: 600000 }, [
|
|
1164
|
+
paragraphXml('\u2014\u2014 ' + subtitle, { size: 1600, color: p.muted, font: theme.fonts.body }),
|
|
1165
|
+
]))
|
|
1166
|
+
}
|
|
1167
|
+
} else if (slide.layout === 'table') {
|
|
1168
|
+
const hasTitle = title !== '' && title !== kicker
|
|
1169
|
+
if (kicker !== '') {
|
|
1170
|
+
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1171
|
+
paragraphXml(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1172
|
+
]))
|
|
1173
|
+
}
|
|
1174
|
+
if (hasTitle) {
|
|
1175
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1176
|
+
paragraphXml(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1177
|
+
]))
|
|
1178
|
+
}
|
|
1179
|
+
shapes.push(tableGraphicXml(idBase + 3, 'Table', { x: 900000, y: hasTitle ? 1950000 : 900000, w: 10300000 }, Array.isArray(slide.rows) ? slide.rows : [], theme))
|
|
917
1180
|
} else if (slide.layout === 'closing') {
|
|
918
1181
|
shapes.push(textShapeXml(idBase + 1, 'Title', { x: 1050000, y: 2300000, w: 10200000, h: 1700000 }, [
|
|
919
1182
|
paragraphXml(title || '谢谢', { size: 5200, color: p.fg, bold: true, align: 'ctr', font }),
|
|
@@ -961,34 +1224,93 @@ function slideXml(slide, index, total, theme, langId) {
|
|
|
961
1224
|
'</p:sld>'
|
|
962
1225
|
}
|
|
963
1226
|
|
|
964
|
-
function slideRelXml() {
|
|
1227
|
+
function slideRelXml(hasNotes, slideIndex) {
|
|
1228
|
+
const notesRel = hasNotes
|
|
1229
|
+
? '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide' + slideIndex + '.xml"/>'
|
|
1230
|
+
: ''
|
|
965
1231
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
966
1232
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
967
1233
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
1234
|
+
notesRel +
|
|
968
1235
|
'</Relationships>'
|
|
969
1236
|
}
|
|
970
1237
|
|
|
971
|
-
function presentationXml(slideCount) {
|
|
1238
|
+
function presentationXml(slideCount, hasNotesMaster) {
|
|
972
1239
|
const slideIds = Array.from({ length: slideCount }, (_, i) =>
|
|
973
1240
|
'<p:sldId id="' + (256 + i) + '" r:id="rId' + (i + 2) + '"/>').join('')
|
|
1241
|
+
const notesMasterId = hasNotesMaster
|
|
1242
|
+
? '<p:notesMasterIdLst><p:notesMasterId id="2147483649" r:id="rId' + (slideCount + 2) + '"/></p:notesMasterIdLst>'
|
|
1243
|
+
: ''
|
|
974
1244
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
975
1245
|
'<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
976
1246
|
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
977
1247
|
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
978
1248
|
'<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>' +
|
|
979
1249
|
'<p:sldIdLst>' + slideIds + '</p:sldIdLst>' +
|
|
1250
|
+
notesMasterId +
|
|
980
1251
|
'<p:sldSz cx="' + EMU_W + '" cy="' + EMU_H + '" type="screen16x9"/>' +
|
|
981
1252
|
'<p:notesSz cx="6858000" cy="9144000"/>' +
|
|
982
1253
|
'</p:presentation>'
|
|
983
1254
|
}
|
|
984
1255
|
|
|
985
|
-
function presentationRelXml(slideCount) {
|
|
1256
|
+
function presentationRelXml(slideCount, hasNotesMaster) {
|
|
986
1257
|
const slideRels = Array.from({ length: slideCount }, (_, i) =>
|
|
987
1258
|
'<Relationship Id="rId' + (i + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + (i + 1) + '.xml"/>').join('')
|
|
1259
|
+
const notesMasterRel = hasNotesMaster
|
|
1260
|
+
? '<Relationship Id="rId' + (slideCount + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="notesMasters/notesMaster1.xml"/>'
|
|
1261
|
+
: ''
|
|
988
1262
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
989
1263
|
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
990
1264
|
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>' +
|
|
991
1265
|
slideRels +
|
|
1266
|
+
notesMasterRel +
|
|
1267
|
+
'</Relationships>'
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function notesMasterXml() {
|
|
1271
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1272
|
+
'<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
1273
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
1274
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
1275
|
+
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
1276
|
+
'<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
|
|
1277
|
+
'</p:spTree></p:cSld>' +
|
|
1278
|
+
'<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>' +
|
|
1279
|
+
'<p:notesStyle><a:lvl1pPr><a:defRPr sz="1200"/></a:lvl1pPr></p:notesStyle>' +
|
|
1280
|
+
'</p:notesMaster>'
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function notesMasterRelXml() {
|
|
1284
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1285
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
1286
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>' +
|
|
1287
|
+
'</Relationships>'
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function notesSlideXml(notes) {
|
|
1291
|
+
const paragraphs = String(notes ?? '').split('\n')
|
|
1292
|
+
.filter((line) => line.trim() !== '')
|
|
1293
|
+
.map((line) => '<a:p><a:r><a:rPr lang="zh-CN" sz="1200" dirty="0"/><a:t>' + escapeXml(line) + '</a:t></a:r></a:p>')
|
|
1294
|
+
.join('')
|
|
1295
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1296
|
+
'<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
1297
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
1298
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
1299
|
+
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
1300
|
+
'<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>' +
|
|
1301
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr/><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp>' +
|
|
1302
|
+
'<p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr/><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/>' +
|
|
1303
|
+
'<p:txBody><a:bodyPr/><a:lstStyle/>' + paragraphs + '</p:txBody></p:sp>' +
|
|
1304
|
+
'</p:spTree></p:cSld>' +
|
|
1305
|
+
'<p:clrMapOvr><a:overrideClrMapping bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:clrMapOvr>' +
|
|
1306
|
+
'</p:notes>'
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function notesSlideRelXml(slideIndex) {
|
|
1310
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1311
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
1312
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide' + slideIndex + '.xml"/>' +
|
|
1313
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>' +
|
|
992
1314
|
'</Relationships>'
|
|
993
1315
|
}
|
|
994
1316
|
|
|
@@ -1070,9 +1392,14 @@ function themeXml(theme) {
|
|
|
1070
1392
|
'</a:theme>'
|
|
1071
1393
|
}
|
|
1072
1394
|
|
|
1073
|
-
function contentTypesXml(slideCount) {
|
|
1395
|
+
function contentTypesXml(slideCount, noteIndexes) {
|
|
1074
1396
|
const overrides = Array.from({ length: slideCount }, (_, i) =>
|
|
1075
1397
|
'<Override PartName="/ppt/slides/slide' + (i + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>').join('')
|
|
1398
|
+
const notesOverrides = noteIndexes.map((index) =>
|
|
1399
|
+
'<Override PartName="/ppt/notesSlides/notesSlide' + (index + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"/>').join('')
|
|
1400
|
+
const notesMasterOverride = noteIndexes.length > 0
|
|
1401
|
+
? '<Override PartName="/ppt/notesMasters/notesMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"/>'
|
|
1402
|
+
: ''
|
|
1076
1403
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1077
1404
|
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
1078
1405
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
@@ -1083,7 +1410,9 @@ function contentTypesXml(slideCount) {
|
|
|
1083
1410
|
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' +
|
|
1084
1411
|
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
|
|
1085
1412
|
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
|
|
1413
|
+
notesMasterOverride +
|
|
1086
1414
|
overrides +
|
|
1415
|
+
notesOverrides +
|
|
1087
1416
|
'</Types>'
|
|
1088
1417
|
}
|
|
1089
1418
|
|
|
@@ -1106,12 +1435,12 @@ function coreXml(title) {
|
|
|
1106
1435
|
'</cp:coreProperties>'
|
|
1107
1436
|
}
|
|
1108
1437
|
|
|
1109
|
-
function appXml(slideCount) {
|
|
1438
|
+
function appXml(slideCount, notesCount) {
|
|
1110
1439
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1111
1440
|
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" ' +
|
|
1112
1441
|
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">' +
|
|
1113
1442
|
'<Application>dsh-ppt</Application><PresentationFormat>Widescreen</PresentationFormat>' +
|
|
1114
|
-
'<Slides>' + slideCount + '</Slides><Notes>
|
|
1443
|
+
'<Slides>' + slideCount + '</Slides><Notes>' + notesCount + '</Notes><HiddenSlides>0</HiddenSlides>' +
|
|
1115
1444
|
'</Properties>'
|
|
1116
1445
|
}
|
|
1117
1446
|
|
|
@@ -1202,22 +1531,36 @@ export function buildPptx(manifest, themeInput, languageInput) {
|
|
|
1202
1531
|
const theme = themeInput?.id ? themeInput : resolveTheme(themeInput)
|
|
1203
1532
|
const language = languageInput?.id ? languageInput : resolveLanguage(languageInput)
|
|
1204
1533
|
const slides = manifest.slides
|
|
1534
|
+
const noteIndexes = []
|
|
1535
|
+
slides.forEach((slide, index) => {
|
|
1536
|
+
if (typeof slide.notes === 'string' && slide.notes.trim() !== '') noteIndexes.push(index)
|
|
1537
|
+
})
|
|
1538
|
+
const hasNotesMaster = noteIndexes.length > 0
|
|
1205
1539
|
const entries = [
|
|
1206
|
-
{ name: '[Content_Types].xml', data: contentTypesXml(slides.length) },
|
|
1540
|
+
{ name: '[Content_Types].xml', data: contentTypesXml(slides.length, noteIndexes) },
|
|
1207
1541
|
{ name: '_rels/.rels', data: rootRelXml() },
|
|
1208
|
-
{ name: 'docProps/app.xml', data: appXml(slides.length) },
|
|
1542
|
+
{ name: 'docProps/app.xml', data: appXml(slides.length, noteIndexes.length) },
|
|
1209
1543
|
{ name: 'docProps/core.xml', data: coreXml(manifest.title) },
|
|
1210
|
-
{ name: 'ppt/presentation.xml', data: presentationXml(slides.length) },
|
|
1211
|
-
{ name: 'ppt/_rels/presentation.xml.rels', data: presentationRelXml(slides.length) },
|
|
1544
|
+
{ name: 'ppt/presentation.xml', data: presentationXml(slides.length, hasNotesMaster) },
|
|
1545
|
+
{ name: 'ppt/_rels/presentation.xml.rels', data: presentationRelXml(slides.length, hasNotesMaster) },
|
|
1212
1546
|
{ name: 'ppt/slideMasters/slideMaster1.xml', data: slideMasterXml(theme) },
|
|
1213
1547
|
{ name: 'ppt/slideMasters/_rels/slideMaster1.xml.rels', data: slideMasterRelXml() },
|
|
1214
1548
|
{ name: 'ppt/slideLayouts/slideLayout1.xml', data: slideLayoutXml(theme) },
|
|
1215
1549
|
{ name: 'ppt/slideLayouts/_rels/slideLayout1.xml.rels', data: slideLayoutRelXml() },
|
|
1216
1550
|
{ name: 'ppt/theme/theme1.xml', data: themeXml(theme) },
|
|
1217
1551
|
]
|
|
1552
|
+
if (hasNotesMaster) {
|
|
1553
|
+
entries.push({ name: 'ppt/notesMasters/notesMaster1.xml', data: notesMasterXml() })
|
|
1554
|
+
entries.push({ name: 'ppt/notesMasters/_rels/notesMaster1.xml.rels', data: notesMasterRelXml() })
|
|
1555
|
+
}
|
|
1218
1556
|
slides.forEach((slide, index) => {
|
|
1557
|
+
const hasNotes = noteIndexes.includes(index)
|
|
1219
1558
|
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.id) })
|
|
1220
|
-
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml() })
|
|
1559
|
+
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml(hasNotes, index + 1) })
|
|
1560
|
+
if (hasNotes) {
|
|
1561
|
+
entries.push({ name: 'ppt/notesSlides/notesSlide' + (index + 1) + '.xml', data: notesSlideXml(slide.notes) })
|
|
1562
|
+
entries.push({ name: 'ppt/notesSlides/_rels/notesSlide' + (index + 1) + '.xml.rels', data: notesSlideRelXml(index + 1) })
|
|
1563
|
+
}
|
|
1221
1564
|
})
|
|
1222
1565
|
return buildZip(entries)
|
|
1223
1566
|
}
|