dsh-ppt 0.7.0 → 0.8.1
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 +114 -91
- package/LICENSE +21 -21
- package/README.en.md +42 -219
- package/README.md +42 -234
- package/cordis.patch.yml +19 -19
- package/docs/CHANGELOG.en.md +29 -0
- package/docs/FEATURE-PLAN-2026-10-01.md +50 -50
- package/docs/LIBREOFFICE-INTEGRATION-2026-10-01.md +69 -69
- package/docs/USAGE.en.md +194 -0
- package/docs/USAGE.md +209 -0
- package/docs/VALIDATION.md +17 -0
- package/docs/validation/0.8.0.md +10 -0
- package/docs/validation-language-2026-10-03.md +16 -0
- package/lib/client.js +304 -0
- package/lib/execution.d.ts +1 -0
- package/lib/execution.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +15 -1
- package/lib/tools.js +2 -1
- package/lib/types.d.ts +1 -0
- package/lib/workbench-language.d.ts +4 -0
- package/lib/workbench-language.js +65 -0
- package/lib/workbench.d.ts +32 -0
- package/lib/workbench.js +270 -0
- package/package.json +91 -84
- package/skills/dsh-ppt/SKILL.md +74 -74
- package/skills/dsh-ppt/package.json +10 -10
- package/skills/dsh-ppt/references/advanced.md +39 -39
- package/skills/dsh-ppt/references/copywriting.md +59 -59
- package/skills/dsh-ppt/references/syntax.md +44 -44
- package/skills/dsh-ppt/references/themes.md +61 -61
- package/skills/dsh-ppt/references/troubleshooting.md +19 -19
- package/skills/dsh-ppt/scripts/build-deck.mjs +184 -184
- package/skills/dsh-ppt/scripts/deck-advanced.mjs +468 -456
- package/skills/dsh-ppt/scripts/deck-core.mjs +2347 -2347
- package/skills/dsh-ppt/scripts/deck-media.mjs +160 -160
- package/skills/dsh-ppt/scripts/deck-render.mjs +270 -236
|
@@ -1,2378 +1,2378 @@
|
|
|
1
|
-
#! /usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* deck-core.mjs —— dsh-ppt 的零依赖演示文稿引擎。
|
|
4
|
-
*
|
|
5
|
-
* 这是插件工具与裸 SKILL.md 共用的唯一事实源:
|
|
6
|
-
* - DSH 内:ppt_create 工具动态加载本文件
|
|
7
|
-
* - 其他 harness:直接运行同目录的 build-deck.mjs
|
|
8
|
-
*
|
|
9
|
-
* 能力:Markdown / 结构化 slides → 三件套
|
|
10
|
-
* deck.html 独立网页放映(键盘/触屏/打印,无外链)
|
|
11
|
-
* deck.pptx 可编辑 PPTX(16:9,OOXML 由本文件手写,zip 用 node:zlib)
|
|
12
|
-
* deck.json 结构化 deck manifest
|
|
13
|
-
*
|
|
14
|
-
* 零运行时依赖,仅使用 node:fs / node:path / node:zlib。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
|
-
import { resolve as resolvePath, join as joinPath } from 'node:path'
|
|
19
|
-
import { deflateRawSync } from 'node:zlib'
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* deck-core.mjs —— dsh-ppt 的零依赖演示文稿引擎。
|
|
4
|
+
*
|
|
5
|
+
* 这是插件工具与裸 SKILL.md 共用的唯一事实源:
|
|
6
|
+
* - DSH 内:ppt_create 工具动态加载本文件
|
|
7
|
+
* - 其他 harness:直接运行同目录的 build-deck.mjs
|
|
8
|
+
*
|
|
9
|
+
* 能力:Markdown / 结构化 slides → 三件套
|
|
10
|
+
* deck.html 独立网页放映(键盘/触屏/打印,无外链)
|
|
11
|
+
* deck.pptx 可编辑 PPTX(16:9,OOXML 由本文件手写,zip 用 node:zlib)
|
|
12
|
+
* deck.json 结构化 deck manifest
|
|
13
|
+
*
|
|
14
|
+
* 零运行时依赖,仅使用 node:fs / node:path / node:zlib。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { resolve as resolvePath, join as joinPath } from 'node:path'
|
|
19
|
+
import { deflateRawSync } from 'node:zlib'
|
|
20
20
|
import { MEDIA_LAYOUTS, MEDIA_CSS, normalizeMediaSlide, renderMediaHtml, mediaBoxes } from './deck-media.mjs'
|
|
21
21
|
import { renderDelivery } from './deck-delivery.mjs'
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* manifest 版本跟随发布包 package.json;把 skills/dsh-ppt 独立复制到其他
|
|
25
|
-
* harness(没有 package.json)时回退为 0.0.0,避免版本号悄悄漂移。
|
|
26
|
-
*/
|
|
27
|
-
function readDeckVersion() {
|
|
28
|
-
for (const file of ['../../../package.json', '../package.json']) {
|
|
29
|
-
try {
|
|
30
|
-
const pkg = JSON.parse(readFileSync(new URL(file, import.meta.url), 'utf8'))
|
|
31
|
-
if (!['dsh-ppt', 'dsh-ppt-skill'].includes(pkg.name)) continue
|
|
32
|
-
const version = String(pkg.version ?? '').trim()
|
|
33
|
-
if (version) return version
|
|
34
|
-
} catch { /* Copied skill may have no parent manifest. */ }
|
|
35
|
-
}
|
|
36
|
-
return '0.0.0'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const DECK_VERSION = readDeckVersion()
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// 主题
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
export const THEMES = {
|
|
46
|
-
swiss: {
|
|
47
|
-
id: 'swiss',
|
|
48
|
-
name: { zh: '瑞士脉冲', en: 'Swiss Pulse' },
|
|
49
|
-
mood: { zh: '精准、理性、数据', en: 'Precise, rational, data-driven' },
|
|
50
|
-
bestFor: { zh: 'SaaS、数据、开发者工具', en: 'SaaS, data, developer tools' },
|
|
51
|
-
dark: true,
|
|
52
|
-
palette: {
|
|
53
|
-
bg: '#10151B',
|
|
54
|
-
panel: '#161D26',
|
|
55
|
-
fg: '#F5F7FA',
|
|
56
|
-
muted: '#8E9AAA',
|
|
57
|
-
accent: '#2F6BFF',
|
|
58
|
-
accent2: '#FFB300',
|
|
59
|
-
},
|
|
60
|
-
fonts: {
|
|
61
|
-
heading: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
62
|
-
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
velvet: {
|
|
66
|
-
id: 'velvet',
|
|
67
|
-
name: { zh: '天鹅绒标准', en: 'Velvet Standard' },
|
|
68
|
-
mood: { zh: '高级、克制、可信', en: 'Premium, restrained, trustworthy' },
|
|
69
|
-
bestFor: { zh: '高管汇报、品牌、融资路演', en: 'Executive decks, brand, investor pitches' },
|
|
70
|
-
dark: true,
|
|
71
|
-
palette: {
|
|
72
|
-
bg: '#111316',
|
|
73
|
-
panel: '#1A1D22',
|
|
74
|
-
fg: '#F4EFE6',
|
|
75
|
-
muted: '#A79F91',
|
|
76
|
-
accent: '#C9A84C',
|
|
77
|
-
accent2: '#3D4A63',
|
|
78
|
-
},
|
|
79
|
-
fonts: {
|
|
80
|
-
heading: 'Georgia, "Times New Roman", "Songti SC", "SimSun", serif',
|
|
81
|
-
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
data: {
|
|
85
|
-
id: 'data',
|
|
86
|
-
name: { zh: '数据漂移', en: 'Data Drift' },
|
|
87
|
-
mood: { zh: '未来、沉浸、前沿', en: 'Futuristic, immersive, cutting-edge' },
|
|
88
|
-
bestFor: { zh: 'AI、数据、研究、技术发布', en: 'AI, data, research, tech launches' },
|
|
89
|
-
dark: true,
|
|
90
|
-
palette: {
|
|
91
|
-
bg: '#070B14',
|
|
92
|
-
panel: '#0D1424',
|
|
93
|
-
fg: '#E8F1FF',
|
|
94
|
-
muted: '#7E8BA8',
|
|
95
|
-
accent: '#7C3AED',
|
|
96
|
-
accent2: '#06B6D4',
|
|
97
|
-
},
|
|
98
|
-
fonts: {
|
|
99
|
-
heading: '"Space Grotesk", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
100
|
-
body: '"Space Grotesk", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
soft: {
|
|
104
|
-
id: 'soft',
|
|
105
|
-
name: { zh: '柔和信号', en: 'Soft Signal' },
|
|
106
|
-
mood: { zh: '温暖、亲近、人本', en: 'Warm, intimate, human' },
|
|
107
|
-
bestFor: { zh: '品牌故事、培训、个人分享', en: 'Brand stories, training, personal talks' },
|
|
108
|
-
dark: false,
|
|
109
|
-
palette: {
|
|
110
|
-
bg: '#FFF8EC',
|
|
111
|
-
panel: '#FFF2DE',
|
|
112
|
-
fg: '#3B2F2A',
|
|
113
|
-
muted: '#7E6F68',
|
|
114
|
-
accent: '#E58A2F',
|
|
115
|
-
accent2: '#8FAF8C',
|
|
116
|
-
},
|
|
117
|
-
fonts: {
|
|
118
|
-
heading: '"Avenir Next", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
119
|
-
body: '"Avenir Next", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
bold: {
|
|
123
|
-
id: 'bold',
|
|
124
|
-
name: { zh: '极繁大字', en: 'Maximalist Type' },
|
|
125
|
-
mood: { zh: '大声、动能、发布', en: 'Loud, kinetic, launch' },
|
|
126
|
-
bestFor: { zh: '产品发布、活动、品牌大事件', en: 'Product launches, events, brand moments' },
|
|
127
|
-
dark: true,
|
|
128
|
-
palette: {
|
|
129
|
-
bg: '#0D0D0D',
|
|
130
|
-
panel: '#181818',
|
|
131
|
-
fg: '#FFFFFF',
|
|
132
|
-
muted: '#B8B8B8',
|
|
133
|
-
accent: '#E63946',
|
|
134
|
-
accent2: '#FFD60A',
|
|
135
|
-
},
|
|
136
|
-
fonts: {
|
|
137
|
-
heading: 'Impact, "Arial Black", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
138
|
-
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export const THEME_IDS = Object.keys(THEMES)
|
|
144
|
-
export const DEFAULT_THEME = 'data'
|
|
145
|
-
|
|
146
|
-
export const LANGUAGES = {
|
|
147
|
-
zh: {
|
|
148
|
-
id: 'zh',
|
|
149
|
-
attr: 'zh-CN',
|
|
150
|
-
ui: {
|
|
151
|
-
slide: '第',
|
|
152
|
-
of: '/ 共',
|
|
153
|
-
theme: '主题',
|
|
154
|
-
help: '← → 翻页 · F 全屏 · G 总览 · S 备注 · P 打印',
|
|
155
|
-
coverKicker: '开场',
|
|
156
|
-
pointKicker: '要点',
|
|
157
|
-
statementKicker: '核心观点',
|
|
158
|
-
quoteKicker: '金句',
|
|
159
|
-
tableKicker: '数据',
|
|
160
|
-
continuation: '(续)',
|
|
161
|
-
notesLabel: '演讲者备注',
|
|
162
|
-
notesToggle: '备注',
|
|
163
|
-
closingTitle: '谢谢',
|
|
164
|
-
closingSubtitle: '讨论与问答',
|
|
165
|
-
generatedBy: '由 dsh-ppt 生成',
|
|
166
|
-
overviewToggle: '总览',
|
|
167
|
-
presenterToggle: '演讲者',
|
|
168
|
-
presenterLabel: '演讲者视图 · Presenter view',
|
|
169
|
-
helpTitle: '快捷键 · Shortcuts',
|
|
170
|
-
noNotes: '(本页无备注)',
|
|
171
|
-
closeLabel: '关闭',
|
|
172
|
-
currentLabel: '当前页 · Current',
|
|
173
|
-
nextLabel: '下一页 · Next',
|
|
174
|
-
timerLabel: '计时 · Timer',
|
|
175
|
-
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示,观众看不到',
|
|
176
|
-
wrapHint: '已环绕到',
|
|
177
|
-
popupBlocked: '浏览器拦截了演讲者窗口,请允许弹出窗口后按 V 重试',
|
|
178
|
-
fullscreenUnavailable: '当前窗口无法进入全屏,请在浏览器中独立打开演示文件后重试。',
|
|
179
|
-
notesCloseLabel: '关闭备注',
|
|
180
|
-
overviewHint: '点击缩略图跳页,方向键切换,Enter 进入,Esc 退出'
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
en: {
|
|
184
|
-
id: 'en',
|
|
185
|
-
attr: 'en-US',
|
|
186
|
-
ui: {
|
|
187
|
-
slide: 'Slide',
|
|
188
|
-
of: '/',
|
|
189
|
-
theme: 'Theme',
|
|
190
|
-
help: '← → navigate · F fullscreen · G overview · S notes · P print',
|
|
191
|
-
coverKicker: 'Opening',
|
|
192
|
-
pointKicker: 'Key point',
|
|
193
|
-
statementKicker: 'Core idea',
|
|
194
|
-
quoteKicker: 'Quote',
|
|
195
|
-
tableKicker: 'Data',
|
|
196
|
-
continuation: ' (continued)',
|
|
197
|
-
notesLabel: 'Speaker notes',
|
|
198
|
-
notesToggle: 'Notes',
|
|
199
|
-
closingTitle: 'Thank You',
|
|
200
|
-
closingSubtitle: 'Discussion & Q&A',
|
|
201
|
-
generatedBy: 'Generated with dsh-ppt',
|
|
202
|
-
overviewToggle: 'Overview',
|
|
203
|
-
presenterToggle: 'Presenter',
|
|
204
|
-
presenterLabel: 'Presenter view',
|
|
205
|
-
helpTitle: 'Keyboard shortcuts',
|
|
206
|
-
noNotes: '(No notes on this slide)',
|
|
207
|
-
closeLabel: 'Close',
|
|
208
|
-
currentLabel: 'Current',
|
|
209
|
-
nextLabel: 'Next',
|
|
210
|
-
timerLabel: 'Timer',
|
|
211
|
-
presenterHint: 'Notes: S shows them on top until a presenter window is open; afterwards only in the presenter window.',
|
|
212
|
-
wrapHint: 'Wrapped to',
|
|
213
|
-
popupBlocked: 'The presenter window was blocked. Allow pop-ups and press V to retry.',
|
|
214
|
-
fullscreenUnavailable: 'Fullscreen is unavailable here. Open the presentation in a browser tab and try again.',
|
|
215
|
-
notesCloseLabel: 'Close notes',
|
|
216
|
-
overviewHint: 'Click a thumbnail to jump; arrows move, Enter opens, Esc exits'
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
bilingual: {
|
|
220
|
-
id: 'bilingual',
|
|
221
|
-
attr: 'zh-CN',
|
|
222
|
-
ui: {
|
|
223
|
-
slide: '第',
|
|
224
|
-
of: '/ 共',
|
|
225
|
-
theme: '主题 · Theme',
|
|
226
|
-
help: '← → 翻页 · F 全屏 · G 总览 · S 备注 · P 打印',
|
|
227
|
-
coverKicker: '开场 · Opening',
|
|
228
|
-
pointKicker: '要点 · Key point',
|
|
229
|
-
statementKicker: '核心观点 · Core Idea',
|
|
230
|
-
quoteKicker: '金句 · Quote',
|
|
231
|
-
tableKicker: '数据 · Data',
|
|
232
|
-
continuation: '(续 · continued)',
|
|
233
|
-
notesLabel: '演讲者备注 · Speaker notes',
|
|
234
|
-
notesToggle: '备注 · Notes',
|
|
235
|
-
closingTitle: '谢谢 · Thank You',
|
|
236
|
-
closingSubtitle: '讨论与问答 · Q&A',
|
|
237
|
-
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
238
|
-
overviewToggle: '总览 · Overview',
|
|
239
|
-
presenterToggle: '演讲者 · Presenter',
|
|
240
|
-
presenterLabel: '演讲者视图 · Presenter view',
|
|
241
|
-
helpTitle: '快捷键 · Shortcuts',
|
|
242
|
-
noNotes: '(本页无备注 · No notes on this slide)',
|
|
243
|
-
closeLabel: '关闭 · Close',
|
|
244
|
-
currentLabel: '当前页 · Current',
|
|
245
|
-
nextLabel: '下一页 · Next',
|
|
246
|
-
timerLabel: '计时 · Timer',
|
|
247
|
-
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示 · Notes: S shows on top until a presenter window is open',
|
|
248
|
-
wrapHint: '已环绕到 · Wrapped to',
|
|
249
|
-
popupBlocked: '浏览器拦截了演讲者窗口 · Presenter window blocked; allow pop-ups and press V',
|
|
250
|
-
fullscreenUnavailable: '当前窗口无法全屏,请独立打开演示文件 · Open this presentation in a browser tab for fullscreen.',
|
|
251
|
-
notesCloseLabel: '关闭备注 · Close notes',
|
|
252
|
-
overviewHint: '点击缩略图跳页 · Click to jump, arrows move, Enter opens, Esc exits'
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// 主题预览(ppt_themes preview=true / buildThemePreview)
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
function themePages(lang) {
|
|
262
|
-
return lang === 'en'
|
|
263
|
-
? { title: 'dsh-ppt theme gallery', hint: 'Pick a theme id for ppt_create', usage: 'theme', dark: 'dark', light: 'light', palette: 'Palette' }
|
|
264
|
-
: { title: 'dsh-ppt 主题预览', hint: '把 id 填给 ppt_create 的 theme 参数', usage: 'theme', dark: '深色', light: '浅色', palette: '色板' }
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** 单套主题的 960x540 SVG 色板卡:可直接放进 README / npm 首屏。 */
|
|
268
|
-
export function renderThemeSvg(input, lang = 'zh') {
|
|
269
|
-
const theme = typeof input === 'string' ? resolveTheme(input) : input
|
|
270
|
-
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
271
|
-
const p = theme.palette
|
|
272
|
-
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
273
|
-
const font = theme.fonts.heading.replace(/"/g, "'")
|
|
274
|
-
const bodyFont = theme.fonts.body.replace(/"/g, "'")
|
|
275
|
-
const out = []
|
|
276
|
-
out.push('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-label="' + escapeXml(theme.id + ' ' + pick(theme.name)) + '">')
|
|
277
|
-
out.push('<rect width="960" height="540" fill="' + p.bg + '"/>')
|
|
278
|
-
out.push('<rect x="0" y="0" width="960" height="6" fill="' + p.accent + '"/><rect x="0" y="534" width="960" height="6" fill="' + p.accent2 + '"/>')
|
|
279
|
-
out.push('<rect x="36" y="40" width="560" height="360" rx="16" fill="' + p.panel + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
280
|
-
out.push('<text x="64" y="92" fill="' + p.accent + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700" letter-spacing="3">' + escapeXml(pick(theme.mood).toUpperCase()) + '</text>')
|
|
281
|
-
out.push('<text x="64" y="158" fill="' + p.fg + '" font-family="' + escapeXml(font) + '" font-size="42" font-weight="800">' + escapeXml('Aa ' + pick(theme.name)) + '</text>')
|
|
282
|
-
out.push('<circle cx="72" cy="206" r="5" fill="' + p.accent + '"/><rect x="92" y="199" width="360" height="12" rx="6" fill="' + hexToRgba(p.fg, 0.75) + '"/>')
|
|
283
|
-
out.push('<circle cx="72" cy="240" r="5" fill="' + p.accent + '"/><rect x="92" y="233" width="300" height="12" rx="6" fill="' + hexToRgba(p.fg, 0.55) + '"/>')
|
|
284
|
-
out.push('<circle cx="72" cy="274" r="5" fill="' + p.accent + '"/><rect x="92" y="267" width="330" height="12" rx="6" fill="' + hexToRgba(p.fg, 0.55) + '"/>')
|
|
285
|
-
out.push('<rect x="64" y="312" width="500" height="34" rx="6" fill="' + p.accent + '"/>')
|
|
286
|
-
out.push('<text x="78" y="334" fill="' + p.bg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700">dsh-ppt</text>')
|
|
287
|
-
out.push('<text x="470" y="334" fill="' + p.bg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700" text-anchor="end">1,234</text>')
|
|
288
|
-
out.push('<rect x="64" y="346" width="500" height="2" fill="' + hexToRgba(p.muted, 0.5) + '"/>')
|
|
289
|
-
out.push('<text x="64" y="378" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13">' + escapeXml(theme.id + ' · ' + (theme.dark ? L.dark : L.light) + ' · v' + DECK_VERSION) + '</text>')
|
|
290
|
-
const swatches = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
291
|
-
const labels = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
292
|
-
out.push('<text x="632" y="76" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13" font-weight="700" letter-spacing="2">' + escapeXml(L.palette.toUpperCase()) + '</text>')
|
|
293
|
-
swatches.forEach((key, i) => {
|
|
294
|
-
const y = 96 + i * 62
|
|
295
|
-
out.push('<rect x="632" y="' + y + '" width="44" height="44" rx="10" fill="' + p[key] + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
296
|
-
out.push('<text x="692" y="' + (y + 20) + '" fill="' + p.fg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="600">' + escapeXml(labels[i]) + '</text>')
|
|
297
|
-
out.push('<text x="692" y="' + (y + 38) + '" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13">' + escapeXml(String(p[key]).toUpperCase()) + '</text>')
|
|
298
|
-
})
|
|
299
|
-
out.push('<text x="632" y="508" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="12">' + escapeXml(pick(theme.bestFor)).slice(0, 44) + '</text>')
|
|
300
|
-
out.push('</svg>')
|
|
301
|
-
return out.join('')
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/** 自包含主题对比页:5 套主题并排,含色板与用法。 */
|
|
305
|
-
export function renderThemePreviewHtml(lang = 'zh') {
|
|
306
|
-
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
307
|
-
const cards = THEME_IDS.map((id) => {
|
|
308
|
-
const theme = THEMES[id]
|
|
309
|
-
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
310
|
-
const chips = Object.keys(theme.palette).map((key) =>
|
|
311
|
-
'<span class="chip"><i style="background:' + theme.palette[key] + '"></i>' + escapeHtml(String(theme.palette[key]).toUpperCase()) + '</span>').join('')
|
|
312
|
-
return '<figure class="card" id="theme-' + escapeHtml(id) + '">' +
|
|
313
|
-
renderThemeSvg(theme, lang === 'en' ? 'en' : 'zh') +
|
|
314
|
-
'<figcaption><div class="row"><strong>' + escapeHtml(id) + '</strong><span class="badge">' + escapeHtml(pick(theme.name)) + '</span></div>' +
|
|
315
|
-
'<p class="muted">' + escapeHtml(pick(theme.mood)) + ' · ' + escapeHtml(pick(theme.bestFor)) + '</p>' +
|
|
316
|
-
'<p class="chips">' + chips + '</p>' +
|
|
317
|
-
'<p class="usage">' + escapeHtml(L.usage) + ': <code>' + escapeHtml(id) + '</code></p></figcaption></figure>'
|
|
318
|
-
}).join('')
|
|
319
|
-
return '<!DOCTYPE html>' + eolSafe('<html lang="zh-CN">') +
|
|
320
|
-
'<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">' +
|
|
321
|
-
'<title>' + escapeHtml(L.title) + '</title><style>' +
|
|
322
|
-
'body{margin:0;background:#0b0f17;color:#e8f1ff;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;padding:28px}' +
|
|
323
|
-
'h1{font-size:26px;margin:0 0 6px}p.sub{color:#8b98ad;margin:0 0 24px;font-size:14px}' +
|
|
324
|
-
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));gap:22px}' +
|
|
325
|
-
'.card{margin:0;background:#111827;border:1px solid #263043;border-radius:16px;overflow:hidden}' +
|
|
326
|
-
'.card svg{display:block;width:100%;height:auto}' +
|
|
327
|
-
'figcaption{padding:14px 16px 16px}.row{display:flex;align-items:center;gap:10px;font-size:16px}' +
|
|
328
|
-
'.badge{color:#8b98ad;font-size:13px}.muted{color:#8b98ad;font-size:13px;margin:6px 0 10px;line-height:1.5}' +
|
|
329
|
-
'.chips{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px}.chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#aab6c9;background:#0b0f17;border:1px solid #263043;border-radius:99px;padding:3px 9px}' +
|
|
330
|
-
'.chip i{width:10px;height:10px;border-radius:3px;display:inline-block}' +
|
|
331
|
-
'.usage{margin:0;font-size:13px;color:#aab6c9}code{background:#0b0f17;border:1px solid #263043;border-radius:6px;padding:1px 6px}' +
|
|
332
|
-
'@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}' +
|
|
333
|
-
'</style></head><body><h1>' + escapeHtml(L.title) + '</h1><p class="sub">' + escapeHtml(L.hint) + ' · dsh-ppt v' + escapeHtml(DECK_VERSION) + '</p>' +
|
|
334
|
-
'<div class="grid">' + cards + '</div></body></html>'
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function eolSafe(text) { return text }
|
|
338
|
-
|
|
339
|
-
/** 写主题对比页 + 每套主题 SVG 色板卡;默认不覆盖同名文件。 */
|
|
340
|
-
export function buildThemePreview(options = {}) {
|
|
341
|
-
const langInput = typeof options.lang === 'string' && options.lang.trim() !== '' ? options.lang.trim() : 'zh'
|
|
342
|
-
const language = resolveLanguage(langInput)
|
|
343
|
-
const outputDir = resolvePath(String(options.outputDir ?? '.'))
|
|
344
|
-
mkdirSync(outputDir, { recursive: true })
|
|
345
|
-
const overwrite = options.overwrite === true
|
|
346
|
-
const pickPath = (base, ext) => {
|
|
347
|
-
const first = resolvePath(joinPath(outputDir, base + ext))
|
|
348
|
-
if (overwrite || !existsSync(first)) return first
|
|
349
|
-
for (let i = 1; i < 1000; i += 1) {
|
|
350
|
-
const candidate = resolvePath(joinPath(outputDir, base + '-' + i + ext))
|
|
351
|
-
if (!existsSync(candidate)) return candidate
|
|
352
|
-
}
|
|
353
|
-
throw new Error('dsh-ppt:找不到可用的预览文件名(同名前缀超过 999 个),请允许 overwrite。')
|
|
354
|
-
}
|
|
355
|
-
const htmlPath = pickPath('themes-preview', '.html')
|
|
356
|
-
writeFileSync(htmlPath, renderThemePreviewHtml(language.id), 'utf8')
|
|
357
|
-
const svgs = THEME_IDS.map((id) => {
|
|
358
|
-
const svgPath = pickPath('theme-' + id, '.svg')
|
|
359
|
-
writeFileSync(svgPath, renderThemeSvg(id, language.id), 'utf8')
|
|
360
|
-
return { id, path: svgPath }
|
|
361
|
-
})
|
|
362
|
-
return { ok: true, outputDir, htmlPath, svgs, themeCount: THEME_IDS.length, language: language.id }
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
export function resolveTheme(input) {
|
|
367
|
-
const id = String(input ?? DEFAULT_THEME).trim().toLowerCase()
|
|
368
|
-
const theme = THEMES[id]
|
|
369
|
-
if (!theme) {
|
|
370
|
-
throw new Error('dsh-ppt:未知主题 "' + id + '",可选:' + THEME_IDS.join(' / ') + '(默认 ' + DEFAULT_THEME + ')')
|
|
371
|
-
}
|
|
372
|
-
return theme
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function resolveLanguage(input) {
|
|
376
|
-
const id = String(input ?? 'zh').trim().toLowerCase()
|
|
377
|
-
const language = LANGUAGES[id]
|
|
378
|
-
if (!language) {
|
|
379
|
-
throw new Error('dsh-ppt:未知语言 "' + id + '",可选:zh / en / bilingual')
|
|
380
|
-
}
|
|
381
|
-
return language
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
export function resolveMotion(input) {
|
|
385
|
-
if (input === undefined || input === null || input === '') return true
|
|
386
|
-
if (input === true || input === 'on') return true
|
|
387
|
-
if (input === false || input === 'off') return false
|
|
388
|
-
throw new Error('dsh-ppt:未知 motion 值 "' + String(input) + '",可选:on / off(默认 on)')
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
export function listThemes(lang = 'zh') {
|
|
392
|
-
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
393
|
-
return THEME_IDS.map((id) => {
|
|
394
|
-
const theme = THEMES[id]
|
|
395
|
-
return {
|
|
396
|
-
id,
|
|
397
|
-
name: pick(theme.name),
|
|
398
|
-
mood: pick(theme.mood),
|
|
399
|
-
bestFor: pick(theme.bestFor),
|
|
400
|
-
dark: theme.dark,
|
|
401
|
-
palette: { ...theme.palette },
|
|
402
|
-
fonts: { ...theme.fonts },
|
|
403
|
-
}
|
|
404
|
-
})
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// ---------------------------------------------------------------------------
|
|
408
|
-
// 文本工具
|
|
409
|
-
// ---------------------------------------------------------------------------
|
|
410
|
-
|
|
411
|
-
export function clampInt(value, fallback, min, max) {
|
|
412
|
-
const n = typeof value === 'number' ? Math.trunc(value) : fallback
|
|
413
|
-
if (!Number.isFinite(n)) return fallback
|
|
414
|
-
return Math.min(max, Math.max(min, n))
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Windows 保留设备名:首个点之前的主名命中即不可用(NUL、com1、aux.report.html 都算)。
|
|
418
|
-
const WINDOWS_RESERVED_FILE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i
|
|
419
|
-
|
|
420
|
-
export function sanitizeFileName(input) {
|
|
421
|
-
const base = String(input ?? 'deck').trim().replace(/\.(html?|pptx|json)$/i, '')
|
|
422
|
-
let cleaned = base
|
|
423
|
-
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '-')
|
|
424
|
-
.replace(/\s+/g, '-')
|
|
425
|
-
.replace(/-+/g, '-')
|
|
426
|
-
.replace(/^-+|-+$/g, '')
|
|
427
|
-
if (WINDOWS_RESERVED_FILE_NAME.test(cleaned)) cleaned = 'deck-' + cleaned
|
|
428
|
-
return cleaned.slice(0, 120) || 'deck'
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const DECK_ARTIFACT_EXTENSIONS = ['.html', '.pptx', '.json']
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* 为三件套选择同一个可用文件名前缀。默认不覆盖:任一产物已存在时,
|
|
435
|
-
* 整组改用 -1/-2… 后缀,避免新旧 deck 被静默混写。
|
|
436
|
-
*/
|
|
437
|
-
export function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
|
|
438
|
-
// 一次 readdir 建集,替代逐个候选 existsSync 探测(最坏 ~3000 次 stat);
|
|
439
|
-
// 统一小写比较,避免 Windows 大小写不敏感文件系统把已占用的名字判成可用。
|
|
440
|
-
const existing = new Set(readdirSync(outputDir).map((name) => name.toLowerCase()))
|
|
441
|
-
const isAvailable = (candidate) => DECK_ARTIFACT_EXTENSIONS.every((ext) => !existing.has((candidate + ext).toLowerCase()))
|
|
442
|
-
if (overwrite || isAvailable(requestedFileName)) return requestedFileName
|
|
443
|
-
for (let index = 1; index < 1000; index += 1) {
|
|
444
|
-
const candidate = requestedFileName + '-' + index
|
|
445
|
-
if (isAvailable(candidate)) return candidate
|
|
446
|
-
}
|
|
447
|
-
throw new Error('dsh-ppt:找不到可用的输出文件名(同名前缀超过 999 个),请更换 fileName 或显式允许 overwrite。')
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
export function escapeXml(value) {
|
|
451
|
-
return String(value ?? '')
|
|
452
|
-
.replace(/&/g, '&')
|
|
453
|
-
.replace(/</g, '<')
|
|
454
|
-
.replace(/>/g, '>')
|
|
455
|
-
.replace(/"/g, '"')
|
|
456
|
-
.replace(/'/g, ''')
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
export function escapeHtml(value) {
|
|
460
|
-
return String(value ?? '')
|
|
461
|
-
.replace(/&/g, '&')
|
|
462
|
-
.replace(/</g, '<')
|
|
463
|
-
.replace(/>/g, '>')
|
|
464
|
-
.replace(/"/g, '"')
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export function stripInlineMarkdown(value) {
|
|
468
|
-
return String(value ?? '')
|
|
469
|
-
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
470
|
-
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
471
|
-
.replace(/`([^`]*)`/g, '$1')
|
|
472
|
-
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
473
|
-
.replace(/__([^_]+)__/g, '$1')
|
|
474
|
-
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
475
|
-
.replace(/_([^_\n]+)_/g, '$1')
|
|
476
|
-
.replace(/<[^>]+>/g, '')
|
|
477
|
-
.replace(/\s+/g, ' ')
|
|
478
|
-
.trim()
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function splitSentences(value) {
|
|
482
|
-
const text = String(value ?? '').trim()
|
|
483
|
-
if (text === '') return []
|
|
484
|
-
const parts = text.split(/(?<=[。!?])|(?<=[.!?…])\s+/).map((part) => part.trim()).filter(Boolean)
|
|
485
|
-
return parts.length > 0 ? parts : [text]
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
export function truncate(value, max) {
|
|
489
|
-
const text = String(value ?? '').trim()
|
|
490
|
-
if (text.length <= max) return text
|
|
491
|
-
return text.slice(0, max - 1).replace(/\s+\S*$/, '') + '…'
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function chunk(items, size) {
|
|
495
|
-
const out = []
|
|
496
|
-
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))
|
|
497
|
-
return out
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// ---------------------------------------------------------------------------
|
|
501
|
-
// Markdown → deck
|
|
502
|
-
// ---------------------------------------------------------------------------
|
|
503
|
-
|
|
504
|
-
function createSection(heading = '', level = 0, coverOnly = false) {
|
|
505
|
-
return { heading, level, coverOnly, bullets: [], paragraphs: [], quote: [], table: null, notes: [] }
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function splitTableRow(line) {
|
|
509
|
-
const trimmed = line.trim()
|
|
510
|
-
const match = /^\|(.+)\|$/.exec(trimmed)
|
|
511
|
-
if (match === null) return null
|
|
512
|
-
return match[1].split('|').map((cell) => cell.trim())
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function isTableSeparator(cells) {
|
|
516
|
-
return cells.length > 0 && cells.every((cell) => /^:?-{2,}:?$/.test(cell) || /^:?-+:?$/.test(cell))
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const NOTES_COMMENT = /^<!--\s*(?:notes?|备注)\s*[::]\s*([\s\S]*?)\s*-->$/
|
|
520
|
-
|
|
521
|
-
function quoteSlideFrom(section, ui) {
|
|
522
|
-
const lines = [...section.quote]
|
|
523
|
-
let attribution = ''
|
|
524
|
-
const attrMatch = /^(?:——|—|--|-)\s*(.+)$/.exec(lines[lines.length - 1] ?? '')
|
|
525
|
-
if (attrMatch !== null && lines.length > 1) {
|
|
526
|
-
attribution = attrMatch[1]
|
|
527
|
-
lines.pop()
|
|
528
|
-
}
|
|
529
|
-
return { layout: 'quote', kicker: ui.quoteKicker, title: truncate(lines.join(' '), 220), subtitle: attribution }
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function paginateTable(slide, continuation = '(续)') {
|
|
533
|
-
const rows = slide.rows
|
|
534
|
-
const columns = Math.max(...rows.map(row => row.length))
|
|
535
|
-
if (columns > 8) throw new Error('dsh-ppt:表格最多 8 列,请拆成多个表格后重试 / tables support up to 8 columns')
|
|
536
|
-
if (rows.some(row => row.some(cell => [...cell].length > 60))) {
|
|
537
|
-
throw new Error('dsh-ppt:表格单元格最多 60 个字符,请精简或拆分内容后重试 / cells support up to 60 characters')
|
|
538
|
-
}
|
|
539
|
-
const padded = rows.map(row => Array.from({ length: columns }, (_, i) => row[i] ?? ''))
|
|
540
|
-
const pages = chunk(padded.slice(1), 8)
|
|
541
|
-
if (pages.length === 0) pages.push([])
|
|
542
|
-
return pages.map((body, index) => {
|
|
543
|
-
const { notes, ...rest } = slide
|
|
544
|
-
const title = slide.title + (index === 0 ? '' : continuation)
|
|
545
|
-
// 同名 kicker/title 共用紧凑标题;续页不能凭空多出一行大标题挤压表格。
|
|
546
|
-
return { ...rest, title, kicker: slide.kicker === slide.title ? title : slide.kicker, rows: [padded[0], ...body],
|
|
547
|
-
...(index === 0 && notes ? { notes } : {}) }
|
|
548
|
-
})
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function tableSlidesFrom(section, ui) {
|
|
552
|
-
const table = section.table
|
|
553
|
-
const rows = [table.header, ...table.rows]
|
|
554
|
-
const heading = section.heading || ui.tableKicker
|
|
555
|
-
return paginateTable({ layout: 'table', kicker: heading, title: heading, rows }, ui.continuation)
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/** 把一个 section 里的金句/表格拆成独立页;备注附着到第一张产出页。 */
|
|
559
|
-
function specialSlidesFor(section, ui) {
|
|
560
|
-
const out = []
|
|
561
|
-
if (section.quote.length > 0) out.push(quoteSlideFrom(section, ui))
|
|
562
|
-
if (section.table !== null) out.push(...tableSlidesFrom(section, ui))
|
|
563
|
-
if (out.length > 0 && section.notes.length > 0) out[0].notes = section.notes.join('\n')
|
|
564
|
-
return out
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
568
|
-
const language = resolveLanguage(lang)
|
|
569
|
-
const ui = language.ui
|
|
570
|
-
const title = String(titleInput ?? '').trim()
|
|
571
|
-
let coverTitle = title
|
|
572
|
-
let coverSubtitle = ''
|
|
573
|
-
let coverSubtitleConsumed = false
|
|
574
|
-
|
|
575
|
-
let body = String(content ?? '')
|
|
576
|
-
.replace(/^\uFEFF/, '')
|
|
577
|
-
.replace(/\r\n?/g, '\n')
|
|
578
|
-
// 去除文档级 YAML frontmatter(如果有)
|
|
579
|
-
body = body.replace(/^---[ \t]*\n[\s\S]*?\n---[ \t]*\n?/, '')
|
|
580
|
-
|
|
581
|
-
const sections = []
|
|
582
|
-
let current = null
|
|
583
|
-
let firstH1Seen = false
|
|
584
|
-
|
|
585
|
-
const flush = () => {
|
|
586
|
-
if (current !== null && (current.heading !== '' || current.bullets.length > 0 || current.paragraphs.length > 0
|
|
587
|
-
|| current.table !== null || current.quote.length > 0)) {
|
|
588
|
-
sections.push(current)
|
|
589
|
-
}
|
|
590
|
-
current = null
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
for (const rawLine of body.split('\n')) {
|
|
594
|
-
const line = rawLine.trim()
|
|
595
|
-
if (line === '') {
|
|
596
|
-
// 无标题分组按空行分段,保证「文档无标题」时按段落生成幻灯片
|
|
597
|
-
if (current !== null && current.heading === '') flush()
|
|
598
|
-
continue
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line)
|
|
602
|
-
if (headingMatch !== null) {
|
|
603
|
-
const level = headingMatch[1].length
|
|
604
|
-
const heading = stripInlineMarkdown(headingMatch[2])
|
|
605
|
-
if (level === 1 && !firstH1Seen) {
|
|
606
|
-
firstH1Seen = true
|
|
607
|
-
if (coverTitle === '') coverTitle = heading
|
|
608
|
-
flush()
|
|
609
|
-
current = createSection(heading, level, true)
|
|
610
|
-
continue
|
|
611
|
-
}
|
|
612
|
-
flush()
|
|
613
|
-
current = createSection(heading, level, false)
|
|
614
|
-
continue
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const bulletMatch = /^\s*(?:[-*+]|\d+[.)])\s+(.+)$/.exec(line)
|
|
618
|
-
if (bulletMatch !== null) {
|
|
619
|
-
const bullet = stripInlineMarkdown(bulletMatch[1])
|
|
620
|
-
if (bullet !== '') {
|
|
621
|
-
if (current === null) current = createSection()
|
|
622
|
-
current.bullets.push(bullet)
|
|
623
|
-
}
|
|
624
|
-
continue
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// 演讲者备注:`<!-- 备注: ... -->` / `<!-- note: ... -->` 附到当前页
|
|
628
|
-
const notesMatch = NOTES_COMMENT.exec(line)
|
|
629
|
-
if (notesMatch !== null && notesMatch[1].trim() !== '') {
|
|
630
|
-
if (current === null) current = createSection()
|
|
631
|
-
current.notes.push(notesMatch[1].trim())
|
|
632
|
-
continue
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Markdown 表格:连续的 | ... | 行,第二行是分隔线
|
|
636
|
-
const tableCells = splitTableRow(line)
|
|
637
|
-
if (tableCells !== null) {
|
|
638
|
-
if (current === null) current = createSection()
|
|
639
|
-
if (isTableSeparator(tableCells)) continue
|
|
640
|
-
if (current.table === null) {
|
|
641
|
-
current.table = { header: tableCells.map((cell) => stripInlineMarkdown(cell)), rows: [] }
|
|
642
|
-
} else {
|
|
643
|
-
current.table.rows.push(tableCells.map((cell) => stripInlineMarkdown(cell)))
|
|
644
|
-
}
|
|
645
|
-
continue
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// 金句:`>` 引用块(最后一行 `—— 出处` 识别为署名)
|
|
649
|
-
const quoteMatch = /^>\s?(.*)$/.exec(line)
|
|
650
|
-
if (quoteMatch !== null) {
|
|
651
|
-
const quoted = stripInlineMarkdown(quoteMatch[1])
|
|
652
|
-
if (quoted !== '') {
|
|
653
|
-
if (current === null) current = createSection()
|
|
654
|
-
current.quote.push(quoted)
|
|
655
|
-
}
|
|
656
|
-
continue
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const paragraph = stripInlineMarkdown(line)
|
|
660
|
-
if (paragraph === '') continue
|
|
661
|
-
if (current === null) current = createSection()
|
|
662
|
-
current.paragraphs.push(paragraph)
|
|
663
|
-
}
|
|
664
|
-
flush()
|
|
665
|
-
|
|
666
|
-
const slides = []
|
|
667
|
-
const coverSource = sections.find((section) => section.coverOnly === true)
|
|
668
|
-
if (coverSource !== null && coverSource !== undefined) {
|
|
669
|
-
const coverText = coverSource.paragraphs[0] ?? coverSource.bullets[0] ?? ''
|
|
670
|
-
if (coverSubtitle === '') {
|
|
671
|
-
coverSubtitle = truncate(coverText, 180)
|
|
672
|
-
coverSubtitleConsumed = true
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
let bodySections = sections.filter((section) => section.coverOnly !== true)
|
|
677
|
-
// 首个 H1 既是封面又把所有要点收在封面节里时,把封面副标题之外的剩余要点
|
|
678
|
-
// 提升为一个无标题节,走下面的要点页生成逻辑,避免丢掉内容。
|
|
679
|
-
if (bodySections.length === 0 && coverSource !== null && coverSource !== undefined) {
|
|
680
|
-
const usedParagraph = coverSource.paragraphs.length > 0
|
|
681
|
-
const extras = [
|
|
682
|
-
...(usedParagraph ? coverSource.bullets : coverSource.bullets.slice(1)),
|
|
683
|
-
...coverSource.paragraphs.slice(usedParagraph ? 1 : 0),
|
|
684
|
-
]
|
|
685
|
-
if (extras.length > 0) {
|
|
686
|
-
const synthetic = createSection()
|
|
687
|
-
synthetic.bullets = extras
|
|
688
|
-
bodySections = [synthetic]
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// 表格和引用不能只挂在封面节上,否则首个 H1 后没有 H2 时会丢失。
|
|
693
|
-
if (coverSource && (coverSource.table !== null || coverSource.quote.length > 0)) {
|
|
694
|
-
const special = createSection()
|
|
695
|
-
special.table = coverSource.table
|
|
696
|
-
special.quote = coverSource.quote
|
|
697
|
-
special.notes = coverSource.notes
|
|
698
|
-
bodySections.unshift(special)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
if (bodySections.length === 0) {
|
|
702
|
-
// 一句话 / 无结构输入:封面 + 核心观点 + 结束页(仍是完整的三页演示文稿)
|
|
703
|
-
const allText = coverSource !== null && coverSource !== undefined
|
|
704
|
-
? [...coverSource.bullets, ...coverSource.paragraphs].join(' ')
|
|
705
|
-
: String(content ?? '').trim()
|
|
706
|
-
if (coverSubtitle === '') coverSubtitle = truncate(splitSentences(allText)[0] ?? '', 180)
|
|
707
|
-
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
708
|
-
if (coverSubtitle !== '') {
|
|
709
|
-
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle })
|
|
710
|
-
}
|
|
711
|
-
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
712
|
-
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const hadHeadings = bodySections.some((section) => section.heading !== '')
|
|
716
|
-
|
|
717
|
-
if (!hadHeadings) {
|
|
718
|
-
// 无标题文档:第一段摘要作封面副标题,后续段落拆成要点页
|
|
719
|
-
const ordered = bodySections.map((section) => ({
|
|
720
|
-
section,
|
|
721
|
-
sentences: [...section.bullets, ...section.paragraphs].flatMap((part) => splitSentences(part)),
|
|
722
|
-
}))
|
|
723
|
-
const firstSentence = ordered[0]?.sentences[0] ?? ''
|
|
724
|
-
if (coverSubtitle === '') coverSubtitle = truncate(firstSentence, 180)
|
|
725
|
-
const bodySlides = []
|
|
726
|
-
let pointIndex = 0
|
|
727
|
-
ordered.forEach((group, groupIndex) => {
|
|
728
|
-
const specials = specialSlidesFor(group.section, ui)
|
|
729
|
-
bodySlides.push(...specials)
|
|
730
|
-
const sentences = (groupIndex === 0 && !coverSubtitleConsumed) ? group.sentences.slice(1) : group.sentences
|
|
731
|
-
for (const points of chunk(sentences, 5)) {
|
|
732
|
-
pointIndex += 1
|
|
733
|
-
const slide = {
|
|
734
|
-
layout: 'bullets',
|
|
735
|
-
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
736
|
-
title: truncate(points[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
737
|
-
bullets: points,
|
|
738
|
-
}
|
|
739
|
-
if (specials.length === 0 && group.section.notes.length > 0) slide.notes = group.section.notes.join('\n')
|
|
740
|
-
bodySlides.push(slide)
|
|
741
|
-
}
|
|
742
|
-
})
|
|
743
|
-
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
744
|
-
if (bodySlides.length === 0) {
|
|
745
|
-
if (coverSubtitle !== '') {
|
|
746
|
-
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle || 'Untitled' })
|
|
747
|
-
}
|
|
748
|
-
} else {
|
|
749
|
-
slides.push(...bodySlides)
|
|
750
|
-
}
|
|
751
|
-
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
752
|
-
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
756
|
-
let pointIndex = 0
|
|
757
|
-
for (const section of bodySections) {
|
|
758
|
-
const notes = section.notes.join('\n')
|
|
759
|
-
const specials = specialSlidesFor(section, ui)
|
|
760
|
-
slides.push(...specials)
|
|
761
|
-
if (section.heading === '') {
|
|
762
|
-
const points = [...section.bullets, ...section.paragraphs]
|
|
763
|
-
.flatMap((part) => splitSentences(part))
|
|
764
|
-
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
765
|
-
for (const [pageIndex, pagePoints] of chunk(points, 8).entries()) {
|
|
766
|
-
pointIndex += 1
|
|
767
|
-
const slide = {
|
|
768
|
-
layout: 'bullets',
|
|
769
|
-
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
770
|
-
title: truncate(pagePoints[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
771
|
-
bullets: pagePoints,
|
|
772
|
-
}
|
|
773
|
-
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
774
|
-
slides.push(slide)
|
|
775
|
-
}
|
|
776
|
-
continue
|
|
777
|
-
}
|
|
778
|
-
const points = [
|
|
779
|
-
...section.bullets,
|
|
780
|
-
...section.paragraphs.flatMap((part) => splitSentences(part)),
|
|
781
|
-
]
|
|
782
|
-
const pages = chunk(points, 8)
|
|
783
|
-
if (pages.length > 0) {
|
|
784
|
-
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
785
|
-
pages.forEach((pagePoints, pageIndex) => {
|
|
786
|
-
const label = pageIndex === 0 ? section.heading : section.heading + ui.continuation
|
|
787
|
-
const slide = { layout: 'bullets', kicker: label, title: label, bullets: pagePoints }
|
|
788
|
-
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
789
|
-
slides.push(slide)
|
|
790
|
-
})
|
|
791
|
-
} else if (specials.length === 0) {
|
|
792
|
-
const slide = { layout: 'section', kicker: section.heading, title: section.heading }
|
|
793
|
-
if (notes !== '') slide.notes = notes
|
|
794
|
-
slides.push(slide)
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
798
|
-
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing', 'quote', 'table', ...MEDIA_LAYOUTS])
|
|
802
|
-
|
|
803
|
-
function normalizeSlide(raw, index) {
|
|
804
|
-
const source = (raw !== null && typeof raw === 'object') ? raw : {}
|
|
805
|
-
const layout = SLIDE_LAYOUTS.has(source.layout) ? source.layout : source.chart ? 'chart' : source.image ? 'image-right' : 'bullets'
|
|
806
|
-
const title = stripInlineMarkdown(source.title ?? '')
|
|
807
|
-
const subtitle = stripInlineMarkdown(source.subtitle ?? '')
|
|
808
|
-
const kicker = stripInlineMarkdown(source.kicker ?? '')
|
|
809
|
-
const text = stripInlineMarkdown(source.text ?? '')
|
|
810
|
-
// 备注保留换行(放映面板与备注页都按原文显示),只做转义,不做 Markdown 清洗
|
|
811
|
-
const notes = String(source.notes ?? '').trim()
|
|
812
|
-
let bullets = []
|
|
813
|
-
if (Array.isArray(source.bullets)) {
|
|
814
|
-
bullets = source.bullets.map((item) => stripInlineMarkdown(String(item))).filter(Boolean)
|
|
815
|
-
} else if (typeof source.bullets === 'string' && source.bullets.trim() !== '') {
|
|
816
|
-
bullets = splitSentences(source.bullets)
|
|
817
|
-
}
|
|
818
|
-
if (MEDIA_LAYOUTS.has(layout)) return normalizeMediaSlide(source, { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }, index)
|
|
819
|
-
if (layout === 'statement' && title === '' && text !== '') {
|
|
820
|
-
return { layout, kicker, title: text, subtitle: subtitle || '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
821
|
-
}
|
|
822
|
-
if (layout === 'quote') {
|
|
823
|
-
const quoteText = title !== '' ? title : text
|
|
824
|
-
if (quoteText === '') return { layout: 'statement', kicker, title: subtitle, subtitle: '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
825
|
-
return { layout, kicker, title: truncate(quoteText, 220), subtitle, ...(notes !== '' ? { notes } : {}) }
|
|
826
|
-
}
|
|
827
|
-
if (layout === 'table') {
|
|
828
|
-
let rows = []
|
|
829
|
-
if (Array.isArray(source.rows)) {
|
|
830
|
-
rows = source.rows
|
|
831
|
-
.map((row) => (Array.isArray(row) ? row : [row]).map((cell) => stripInlineMarkdown(String(cell))))
|
|
832
|
-
.filter((row) => row.some((cell) => cell !== ''))
|
|
833
|
-
}
|
|
834
|
-
if (rows.length === 0) {
|
|
835
|
-
// 没有表格数据的 table 页退化成要点页,避免产出空白版式
|
|
836
|
-
return { layout: 'bullets', kicker, title, subtitle, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
837
|
-
}
|
|
838
|
-
return { layout, kicker: kicker || title, title, rows, ...(notes !== '' ? { notes } : {}) }
|
|
839
|
-
}
|
|
840
|
-
return { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function checkSlideLimit(slides, limit) {
|
|
844
|
-
if (slides.length > limit) {
|
|
845
|
-
throw new Error('dsh-ppt:生成后共 ' + slides.length + ' 页,超过 maxSlides=' + limit
|
|
846
|
-
+ ',请提高 maxSlides(最多 120)或拆分内容 / generated ' + slides.length + ' slides; raise maxSlides or split the deck')
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
export function normalizeSlides(rawSlides, maxSlides = 60, lang = 'zh') {
|
|
851
|
-
if (!Array.isArray(rawSlides) || rawSlides.length === 0) {
|
|
852
|
-
throw new Error('dsh-ppt:slides 必须是非空数组(每个元素是 { layout, title, subtitle, kicker, bullets } 对象)')
|
|
853
|
-
}
|
|
854
|
-
const limit = clampInt(maxSlides, 60, 1, 120)
|
|
855
|
-
const ui = resolveLanguage(lang).ui
|
|
856
|
-
const slides = rawSlides.map(normalizeSlide).flatMap(slide => slide.layout === 'table'
|
|
857
|
-
? paginateTable(slide, ui.continuation) : [slide])
|
|
858
|
-
checkSlideLimit(slides, limit)
|
|
859
|
-
if (slides.length === 0) throw new Error('dsh-ppt:slides 规范化后为空')
|
|
860
|
-
return slides
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// ---------------------------------------------------------------------------
|
|
864
|
-
// 构建入口
|
|
865
|
-
// ---------------------------------------------------------------------------
|
|
866
|
-
|
|
867
|
-
export function normalizeBuildOptions(options = {}) {
|
|
868
|
-
const title = String(options.title ?? '').trim()
|
|
869
|
-
if (title === '') throw new Error('dsh-ppt:title 不能为空')
|
|
870
|
-
const theme = resolveTheme(options.theme)
|
|
871
|
-
const language = resolveLanguage(options.lang)
|
|
872
|
-
const motion = resolveMotion(options.motion)
|
|
873
|
-
const maxSlides = clampInt(options.maxSlides, 60, 3, 120)
|
|
874
|
-
let deck
|
|
875
|
-
if (Array.isArray(options.slides) && options.slides.length > 0) {
|
|
876
|
-
deck = {
|
|
877
|
-
title,
|
|
878
|
-
subtitle: stripInlineMarkdown(options.subtitle ?? ''),
|
|
879
|
-
slides: normalizeSlides(options.slides, maxSlides, language.id),
|
|
880
|
-
}
|
|
881
|
-
} else {
|
|
882
|
-
const content = String(options.content ?? '').trim()
|
|
883
|
-
if (content === '') throw new Error('dsh-ppt:content 不能为空(或用 slides 传结构化幻灯片)')
|
|
884
|
-
deck = parseMarkdownDeck(title, content, language.id)
|
|
885
|
-
checkSlideLimit(deck.slides, maxSlides)
|
|
886
|
-
}
|
|
887
|
-
if (deck.slides.length < 1) throw new Error('dsh-ppt:没有可生成的幻灯片')
|
|
888
|
-
const outputDir = resolvePath(String(options.outputDir ?? '.').trim() || '.')
|
|
889
|
-
const fileName = sanitizeFileName(options.fileName ?? title)
|
|
890
|
-
const overwrite = options.overwrite === true
|
|
891
|
-
return { title, theme, language, motion, deck, outputDir, fileName, overwrite }
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* 三件套落盘:三份内容全部先写同目录临时文件,再逐个 rename 提交。
|
|
896
|
-
* 任一步失败都清理临时文件,并把已提交的目标恢复为调用前的内容,
|
|
897
|
-
* 避免出现「json/html 已更新、pptx 还是旧的」这类混版状态。
|
|
898
|
-
*/
|
|
899
|
-
export function commitDeckArtifacts(artifacts) {
|
|
900
|
-
const stamp = process.pid.toString(36) + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8)
|
|
901
|
-
const items = artifacts.map((item, index) => ({ ...item, tempPath: item.path + '.' + stamp + '-' + index + '.tmp' }))
|
|
902
|
-
const removeTemp = (item) => {
|
|
903
|
-
try {
|
|
904
|
-
rmSync(item.tempPath, { force: true })
|
|
905
|
-
} catch {}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// 阶段 0:记下已有产物内容用于回滚;读不出来(如被同名目录占用)先报错,不碰任何文件。
|
|
909
|
-
const previous = new Map()
|
|
910
|
-
for (const item of items) {
|
|
911
|
-
if (!existsSync(item.path)) continue
|
|
912
|
-
try {
|
|
913
|
-
previous.set(item.path, readFileSync(item.path))
|
|
914
|
-
} catch (error) {
|
|
915
|
-
throw new Error('dsh-ppt:三件套写入失败(' + item.path + ' 已存在但无法读取),原文件未改动:' + error.message)
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// 阶段 1:全部写临时文件;这一步失败时目标文件尚未被改动。
|
|
920
|
-
let written = 0
|
|
921
|
-
try {
|
|
922
|
-
for (const item of items) {
|
|
923
|
-
writeFileSync(item.tempPath, item.data, item.encoding)
|
|
924
|
-
written += 1
|
|
925
|
-
}
|
|
926
|
-
} catch (error) {
|
|
927
|
-
for (const item of items.slice(0, written + 1)) removeTemp(item)
|
|
928
|
-
throw new Error('dsh-ppt:写入临时文件失败(' + items[written].path + '),原三件套未改动:' + error.message)
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// 阶段 2:逐个 rename 提交;中途失败则把已提交的目标回滚成旧内容。
|
|
932
|
-
const committed = []
|
|
933
|
-
try {
|
|
934
|
-
for (const item of items) {
|
|
935
|
-
renameSync(item.tempPath, item.path)
|
|
936
|
-
committed.push(item)
|
|
937
|
-
}
|
|
938
|
-
} catch (error) {
|
|
939
|
-
const failedPath = items[committed.length].path
|
|
940
|
-
for (const item of items) removeTemp(item)
|
|
941
|
-
for (const item of committed) {
|
|
942
|
-
try {
|
|
943
|
-
const previousData = previous.get(item.path)
|
|
944
|
-
if (previousData === undefined) rmSync(item.path, { force: true })
|
|
945
|
-
else writeFileSync(item.path, previousData)
|
|
946
|
-
} catch {}
|
|
947
|
-
}
|
|
948
|
-
throw new Error('dsh-ppt:提交三件套失败(' + failedPath + '),已回滚为原文件:' + error.message)
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
export function buildDeck(options = {}) {
|
|
953
|
-
if (options.brand || options.template || options.slides?.some(slide => MEDIA_LAYOUTS.has(slide.layout) || slide.image || slide.chart)) {
|
|
954
|
-
throw new Error('dsh-ppt:图片、图表、模板与品牌请使用异步 buildDeckAsync 或 ppt_create / CLI')
|
|
955
|
-
}
|
|
956
|
-
const normalized = normalizeBuildOptions(options)
|
|
957
|
-
const { title, theme, language, motion, deck, outputDir, overwrite } = normalized
|
|
958
|
-
mkdirSync(outputDir, { recursive: true })
|
|
959
|
-
const fileName = resolveDeckFileName(outputDir, normalized.fileName, overwrite)
|
|
960
|
-
|
|
961
|
-
const manifest = {
|
|
962
|
-
version: DECK_VERSION,
|
|
963
|
-
title,
|
|
964
|
-
theme: theme.id,
|
|
965
|
-
language: language.id,
|
|
966
|
-
motion,
|
|
967
|
-
slideCount: deck.slides.length,
|
|
968
|
-
slides: deck.slides,
|
|
969
|
-
}
|
|
970
|
-
const jsonPath = resolvePath(outputDir, fileName + '.json')
|
|
971
|
-
const htmlPath = resolvePath(outputDir, fileName + '.html')
|
|
972
|
-
const pptxPath = resolvePath(outputDir, fileName + '.pptx')
|
|
973
|
-
|
|
974
|
-
// 先生成三份内容再统一提交:生成阶段抛错时也不会留下半套文件。
|
|
975
|
-
commitDeckArtifacts([
|
|
976
|
-
{ path: jsonPath, data: JSON.stringify(manifest, null, 2) + '\n', encoding: 'utf8' },
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* manifest 版本跟随发布包 package.json;把 skills/dsh-ppt 独立复制到其他
|
|
25
|
+
* harness(没有 package.json)时回退为 0.0.0,避免版本号悄悄漂移。
|
|
26
|
+
*/
|
|
27
|
+
function readDeckVersion() {
|
|
28
|
+
for (const file of ['../../../package.json', '../package.json']) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(new URL(file, import.meta.url), 'utf8'))
|
|
31
|
+
if (!['dsh-ppt', 'dsh-ppt-skill'].includes(pkg.name)) continue
|
|
32
|
+
const version = String(pkg.version ?? '').trim()
|
|
33
|
+
if (version) return version
|
|
34
|
+
} catch { /* Copied skill may have no parent manifest. */ }
|
|
35
|
+
}
|
|
36
|
+
return '0.0.0'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DECK_VERSION = readDeckVersion()
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// 主题
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export const THEMES = {
|
|
46
|
+
swiss: {
|
|
47
|
+
id: 'swiss',
|
|
48
|
+
name: { zh: '瑞士脉冲', en: 'Swiss Pulse' },
|
|
49
|
+
mood: { zh: '精准、理性、数据', en: 'Precise, rational, data-driven' },
|
|
50
|
+
bestFor: { zh: 'SaaS、数据、开发者工具', en: 'SaaS, data, developer tools' },
|
|
51
|
+
dark: true,
|
|
52
|
+
palette: {
|
|
53
|
+
bg: '#10151B',
|
|
54
|
+
panel: '#161D26',
|
|
55
|
+
fg: '#F5F7FA',
|
|
56
|
+
muted: '#8E9AAA',
|
|
57
|
+
accent: '#2F6BFF',
|
|
58
|
+
accent2: '#FFB300',
|
|
59
|
+
},
|
|
60
|
+
fonts: {
|
|
61
|
+
heading: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
62
|
+
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
velvet: {
|
|
66
|
+
id: 'velvet',
|
|
67
|
+
name: { zh: '天鹅绒标准', en: 'Velvet Standard' },
|
|
68
|
+
mood: { zh: '高级、克制、可信', en: 'Premium, restrained, trustworthy' },
|
|
69
|
+
bestFor: { zh: '高管汇报、品牌、融资路演', en: 'Executive decks, brand, investor pitches' },
|
|
70
|
+
dark: true,
|
|
71
|
+
palette: {
|
|
72
|
+
bg: '#111316',
|
|
73
|
+
panel: '#1A1D22',
|
|
74
|
+
fg: '#F4EFE6',
|
|
75
|
+
muted: '#A79F91',
|
|
76
|
+
accent: '#C9A84C',
|
|
77
|
+
accent2: '#3D4A63',
|
|
78
|
+
},
|
|
79
|
+
fonts: {
|
|
80
|
+
heading: 'Georgia, "Times New Roman", "Songti SC", "SimSun", serif',
|
|
81
|
+
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
data: {
|
|
85
|
+
id: 'data',
|
|
86
|
+
name: { zh: '数据漂移', en: 'Data Drift' },
|
|
87
|
+
mood: { zh: '未来、沉浸、前沿', en: 'Futuristic, immersive, cutting-edge' },
|
|
88
|
+
bestFor: { zh: 'AI、数据、研究、技术发布', en: 'AI, data, research, tech launches' },
|
|
89
|
+
dark: true,
|
|
90
|
+
palette: {
|
|
91
|
+
bg: '#070B14',
|
|
92
|
+
panel: '#0D1424',
|
|
93
|
+
fg: '#E8F1FF',
|
|
94
|
+
muted: '#7E8BA8',
|
|
95
|
+
accent: '#7C3AED',
|
|
96
|
+
accent2: '#06B6D4',
|
|
97
|
+
},
|
|
98
|
+
fonts: {
|
|
99
|
+
heading: '"Space Grotesk", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
100
|
+
body: '"Space Grotesk", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
soft: {
|
|
104
|
+
id: 'soft',
|
|
105
|
+
name: { zh: '柔和信号', en: 'Soft Signal' },
|
|
106
|
+
mood: { zh: '温暖、亲近、人本', en: 'Warm, intimate, human' },
|
|
107
|
+
bestFor: { zh: '品牌故事、培训、个人分享', en: 'Brand stories, training, personal talks' },
|
|
108
|
+
dark: false,
|
|
109
|
+
palette: {
|
|
110
|
+
bg: '#FFF8EC',
|
|
111
|
+
panel: '#FFF2DE',
|
|
112
|
+
fg: '#3B2F2A',
|
|
113
|
+
muted: '#7E6F68',
|
|
114
|
+
accent: '#E58A2F',
|
|
115
|
+
accent2: '#8FAF8C',
|
|
116
|
+
},
|
|
117
|
+
fonts: {
|
|
118
|
+
heading: '"Avenir Next", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
119
|
+
body: '"Avenir Next", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
bold: {
|
|
123
|
+
id: 'bold',
|
|
124
|
+
name: { zh: '极繁大字', en: 'Maximalist Type' },
|
|
125
|
+
mood: { zh: '大声、动能、发布', en: 'Loud, kinetic, launch' },
|
|
126
|
+
bestFor: { zh: '产品发布、活动、品牌大事件', en: 'Product launches, events, brand moments' },
|
|
127
|
+
dark: true,
|
|
128
|
+
palette: {
|
|
129
|
+
bg: '#0D0D0D',
|
|
130
|
+
panel: '#181818',
|
|
131
|
+
fg: '#FFFFFF',
|
|
132
|
+
muted: '#B8B8B8',
|
|
133
|
+
accent: '#E63946',
|
|
134
|
+
accent2: '#FFD60A',
|
|
135
|
+
},
|
|
136
|
+
fonts: {
|
|
137
|
+
heading: 'Impact, "Arial Black", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
138
|
+
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const THEME_IDS = Object.keys(THEMES)
|
|
144
|
+
export const DEFAULT_THEME = 'data'
|
|
145
|
+
|
|
146
|
+
export const LANGUAGES = {
|
|
147
|
+
zh: {
|
|
148
|
+
id: 'zh',
|
|
149
|
+
attr: 'zh-CN',
|
|
150
|
+
ui: {
|
|
151
|
+
slide: '第',
|
|
152
|
+
of: '/ 共',
|
|
153
|
+
theme: '主题',
|
|
154
|
+
help: '← → 翻页 · F 全屏 · G 总览 · S 备注 · P 打印',
|
|
155
|
+
coverKicker: '开场',
|
|
156
|
+
pointKicker: '要点',
|
|
157
|
+
statementKicker: '核心观点',
|
|
158
|
+
quoteKicker: '金句',
|
|
159
|
+
tableKicker: '数据',
|
|
160
|
+
continuation: '(续)',
|
|
161
|
+
notesLabel: '演讲者备注',
|
|
162
|
+
notesToggle: '备注',
|
|
163
|
+
closingTitle: '谢谢',
|
|
164
|
+
closingSubtitle: '讨论与问答',
|
|
165
|
+
generatedBy: '由 dsh-ppt 生成',
|
|
166
|
+
overviewToggle: '总览',
|
|
167
|
+
presenterToggle: '演讲者',
|
|
168
|
+
presenterLabel: '演讲者视图 · Presenter view',
|
|
169
|
+
helpTitle: '快捷键 · Shortcuts',
|
|
170
|
+
noNotes: '(本页无备注)',
|
|
171
|
+
closeLabel: '关闭',
|
|
172
|
+
currentLabel: '当前页 · Current',
|
|
173
|
+
nextLabel: '下一页 · Next',
|
|
174
|
+
timerLabel: '计时 · Timer',
|
|
175
|
+
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示,观众看不到',
|
|
176
|
+
wrapHint: '已环绕到',
|
|
177
|
+
popupBlocked: '浏览器拦截了演讲者窗口,请允许弹出窗口后按 V 重试',
|
|
178
|
+
fullscreenUnavailable: '当前窗口无法进入全屏,请在浏览器中独立打开演示文件后重试。',
|
|
179
|
+
notesCloseLabel: '关闭备注',
|
|
180
|
+
overviewHint: '点击缩略图跳页,方向键切换,Enter 进入,Esc 退出'
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
en: {
|
|
184
|
+
id: 'en',
|
|
185
|
+
attr: 'en-US',
|
|
186
|
+
ui: {
|
|
187
|
+
slide: 'Slide',
|
|
188
|
+
of: '/',
|
|
189
|
+
theme: 'Theme',
|
|
190
|
+
help: '← → navigate · F fullscreen · G overview · S notes · P print',
|
|
191
|
+
coverKicker: 'Opening',
|
|
192
|
+
pointKicker: 'Key point',
|
|
193
|
+
statementKicker: 'Core idea',
|
|
194
|
+
quoteKicker: 'Quote',
|
|
195
|
+
tableKicker: 'Data',
|
|
196
|
+
continuation: ' (continued)',
|
|
197
|
+
notesLabel: 'Speaker notes',
|
|
198
|
+
notesToggle: 'Notes',
|
|
199
|
+
closingTitle: 'Thank You',
|
|
200
|
+
closingSubtitle: 'Discussion & Q&A',
|
|
201
|
+
generatedBy: 'Generated with dsh-ppt',
|
|
202
|
+
overviewToggle: 'Overview',
|
|
203
|
+
presenterToggle: 'Presenter',
|
|
204
|
+
presenterLabel: 'Presenter view',
|
|
205
|
+
helpTitle: 'Keyboard shortcuts',
|
|
206
|
+
noNotes: '(No notes on this slide)',
|
|
207
|
+
closeLabel: 'Close',
|
|
208
|
+
currentLabel: 'Current',
|
|
209
|
+
nextLabel: 'Next',
|
|
210
|
+
timerLabel: 'Timer',
|
|
211
|
+
presenterHint: 'Notes: S shows them on top until a presenter window is open; afterwards only in the presenter window.',
|
|
212
|
+
wrapHint: 'Wrapped to',
|
|
213
|
+
popupBlocked: 'The presenter window was blocked. Allow pop-ups and press V to retry.',
|
|
214
|
+
fullscreenUnavailable: 'Fullscreen is unavailable here. Open the presentation in a browser tab and try again.',
|
|
215
|
+
notesCloseLabel: 'Close notes',
|
|
216
|
+
overviewHint: 'Click a thumbnail to jump; arrows move, Enter opens, Esc exits'
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
bilingual: {
|
|
220
|
+
id: 'bilingual',
|
|
221
|
+
attr: 'zh-CN',
|
|
222
|
+
ui: {
|
|
223
|
+
slide: '第',
|
|
224
|
+
of: '/ 共',
|
|
225
|
+
theme: '主题 · Theme',
|
|
226
|
+
help: '← → 翻页 · F 全屏 · G 总览 · S 备注 · P 打印',
|
|
227
|
+
coverKicker: '开场 · Opening',
|
|
228
|
+
pointKicker: '要点 · Key point',
|
|
229
|
+
statementKicker: '核心观点 · Core Idea',
|
|
230
|
+
quoteKicker: '金句 · Quote',
|
|
231
|
+
tableKicker: '数据 · Data',
|
|
232
|
+
continuation: '(续 · continued)',
|
|
233
|
+
notesLabel: '演讲者备注 · Speaker notes',
|
|
234
|
+
notesToggle: '备注 · Notes',
|
|
235
|
+
closingTitle: '谢谢 · Thank You',
|
|
236
|
+
closingSubtitle: '讨论与问答 · Q&A',
|
|
237
|
+
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
238
|
+
overviewToggle: '总览 · Overview',
|
|
239
|
+
presenterToggle: '演讲者 · Presenter',
|
|
240
|
+
presenterLabel: '演讲者视图 · Presenter view',
|
|
241
|
+
helpTitle: '快捷键 · Shortcuts',
|
|
242
|
+
noNotes: '(本页无备注 · No notes on this slide)',
|
|
243
|
+
closeLabel: '关闭 · Close',
|
|
244
|
+
currentLabel: '当前页 · Current',
|
|
245
|
+
nextLabel: '下一页 · Next',
|
|
246
|
+
timerLabel: '计时 · Timer',
|
|
247
|
+
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示 · Notes: S shows on top until a presenter window is open',
|
|
248
|
+
wrapHint: '已环绕到 · Wrapped to',
|
|
249
|
+
popupBlocked: '浏览器拦截了演讲者窗口 · Presenter window blocked; allow pop-ups and press V',
|
|
250
|
+
fullscreenUnavailable: '当前窗口无法全屏,请独立打开演示文件 · Open this presentation in a browser tab for fullscreen.',
|
|
251
|
+
notesCloseLabel: '关闭备注 · Close notes',
|
|
252
|
+
overviewHint: '点击缩略图跳页 · Click to jump, arrows move, Enter opens, Esc exits'
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// 主题预览(ppt_themes preview=true / buildThemePreview)
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
function themePages(lang) {
|
|
262
|
+
return lang === 'en'
|
|
263
|
+
? { title: 'dsh-ppt theme gallery', hint: 'Pick a theme id for ppt_create', usage: 'theme', dark: 'dark', light: 'light', palette: 'Palette' }
|
|
264
|
+
: { title: 'dsh-ppt 主题预览', hint: '把 id 填给 ppt_create 的 theme 参数', usage: 'theme', dark: '深色', light: '浅色', palette: '色板' }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 单套主题的 960x540 SVG 色板卡:可直接放进 README / npm 首屏。 */
|
|
268
|
+
export function renderThemeSvg(input, lang = 'zh') {
|
|
269
|
+
const theme = typeof input === 'string' ? resolveTheme(input) : input
|
|
270
|
+
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
271
|
+
const p = theme.palette
|
|
272
|
+
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
273
|
+
const font = theme.fonts.heading.replace(/"/g, "'")
|
|
274
|
+
const bodyFont = theme.fonts.body.replace(/"/g, "'")
|
|
275
|
+
const out = []
|
|
276
|
+
out.push('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-label="' + escapeXml(theme.id + ' ' + pick(theme.name)) + '">')
|
|
277
|
+
out.push('<rect width="960" height="540" fill="' + p.bg + '"/>')
|
|
278
|
+
out.push('<rect x="0" y="0" width="960" height="6" fill="' + p.accent + '"/><rect x="0" y="534" width="960" height="6" fill="' + p.accent2 + '"/>')
|
|
279
|
+
out.push('<rect x="36" y="40" width="560" height="360" rx="16" fill="' + p.panel + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
280
|
+
out.push('<text x="64" y="92" fill="' + p.accent + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700" letter-spacing="3">' + escapeXml(pick(theme.mood).toUpperCase()) + '</text>')
|
|
281
|
+
out.push('<text x="64" y="158" fill="' + p.fg + '" font-family="' + escapeXml(font) + '" font-size="42" font-weight="800">' + escapeXml('Aa ' + pick(theme.name)) + '</text>')
|
|
282
|
+
out.push('<circle cx="72" cy="206" r="5" fill="' + p.accent + '"/><rect x="92" y="199" width="360" height="12" rx="6" fill="' + hexToRgba(p.fg, 0.75) + '"/>')
|
|
283
|
+
out.push('<circle cx="72" cy="240" r="5" fill="' + p.accent + '"/><rect x="92" y="233" width="300" height="12" rx="6" fill="' + hexToRgba(p.fg, 0.55) + '"/>')
|
|
284
|
+
out.push('<circle cx="72" cy="274" r="5" fill="' + p.accent + '"/><rect x="92" y="267" width="330" height="12" rx="6" fill="' + hexToRgba(p.fg, 0.55) + '"/>')
|
|
285
|
+
out.push('<rect x="64" y="312" width="500" height="34" rx="6" fill="' + p.accent + '"/>')
|
|
286
|
+
out.push('<text x="78" y="334" fill="' + p.bg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700">dsh-ppt</text>')
|
|
287
|
+
out.push('<text x="470" y="334" fill="' + p.bg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700" text-anchor="end">1,234</text>')
|
|
288
|
+
out.push('<rect x="64" y="346" width="500" height="2" fill="' + hexToRgba(p.muted, 0.5) + '"/>')
|
|
289
|
+
out.push('<text x="64" y="378" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13">' + escapeXml(theme.id + ' · ' + (theme.dark ? L.dark : L.light) + ' · v' + DECK_VERSION) + '</text>')
|
|
290
|
+
const swatches = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
291
|
+
const labels = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
292
|
+
out.push('<text x="632" y="76" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13" font-weight="700" letter-spacing="2">' + escapeXml(L.palette.toUpperCase()) + '</text>')
|
|
293
|
+
swatches.forEach((key, i) => {
|
|
294
|
+
const y = 96 + i * 62
|
|
295
|
+
out.push('<rect x="632" y="' + y + '" width="44" height="44" rx="10" fill="' + p[key] + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
296
|
+
out.push('<text x="692" y="' + (y + 20) + '" fill="' + p.fg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="600">' + escapeXml(labels[i]) + '</text>')
|
|
297
|
+
out.push('<text x="692" y="' + (y + 38) + '" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13">' + escapeXml(String(p[key]).toUpperCase()) + '</text>')
|
|
298
|
+
})
|
|
299
|
+
out.push('<text x="632" y="508" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="12">' + escapeXml(pick(theme.bestFor)).slice(0, 44) + '</text>')
|
|
300
|
+
out.push('</svg>')
|
|
301
|
+
return out.join('')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 自包含主题对比页:5 套主题并排,含色板与用法。 */
|
|
305
|
+
export function renderThemePreviewHtml(lang = 'zh') {
|
|
306
|
+
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
307
|
+
const cards = THEME_IDS.map((id) => {
|
|
308
|
+
const theme = THEMES[id]
|
|
309
|
+
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
310
|
+
const chips = Object.keys(theme.palette).map((key) =>
|
|
311
|
+
'<span class="chip"><i style="background:' + theme.palette[key] + '"></i>' + escapeHtml(String(theme.palette[key]).toUpperCase()) + '</span>').join('')
|
|
312
|
+
return '<figure class="card" id="theme-' + escapeHtml(id) + '">' +
|
|
313
|
+
renderThemeSvg(theme, lang === 'en' ? 'en' : 'zh') +
|
|
314
|
+
'<figcaption><div class="row"><strong>' + escapeHtml(id) + '</strong><span class="badge">' + escapeHtml(pick(theme.name)) + '</span></div>' +
|
|
315
|
+
'<p class="muted">' + escapeHtml(pick(theme.mood)) + ' · ' + escapeHtml(pick(theme.bestFor)) + '</p>' +
|
|
316
|
+
'<p class="chips">' + chips + '</p>' +
|
|
317
|
+
'<p class="usage">' + escapeHtml(L.usage) + ': <code>' + escapeHtml(id) + '</code></p></figcaption></figure>'
|
|
318
|
+
}).join('')
|
|
319
|
+
return '<!DOCTYPE html>' + eolSafe('<html lang="zh-CN">') +
|
|
320
|
+
'<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">' +
|
|
321
|
+
'<title>' + escapeHtml(L.title) + '</title><style>' +
|
|
322
|
+
'body{margin:0;background:#0b0f17;color:#e8f1ff;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;padding:28px}' +
|
|
323
|
+
'h1{font-size:26px;margin:0 0 6px}p.sub{color:#8b98ad;margin:0 0 24px;font-size:14px}' +
|
|
324
|
+
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));gap:22px}' +
|
|
325
|
+
'.card{margin:0;background:#111827;border:1px solid #263043;border-radius:16px;overflow:hidden}' +
|
|
326
|
+
'.card svg{display:block;width:100%;height:auto}' +
|
|
327
|
+
'figcaption{padding:14px 16px 16px}.row{display:flex;align-items:center;gap:10px;font-size:16px}' +
|
|
328
|
+
'.badge{color:#8b98ad;font-size:13px}.muted{color:#8b98ad;font-size:13px;margin:6px 0 10px;line-height:1.5}' +
|
|
329
|
+
'.chips{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px}.chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#aab6c9;background:#0b0f17;border:1px solid #263043;border-radius:99px;padding:3px 9px}' +
|
|
330
|
+
'.chip i{width:10px;height:10px;border-radius:3px;display:inline-block}' +
|
|
331
|
+
'.usage{margin:0;font-size:13px;color:#aab6c9}code{background:#0b0f17;border:1px solid #263043;border-radius:6px;padding:1px 6px}' +
|
|
332
|
+
'@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}' +
|
|
333
|
+
'</style></head><body><h1>' + escapeHtml(L.title) + '</h1><p class="sub">' + escapeHtml(L.hint) + ' · dsh-ppt v' + escapeHtml(DECK_VERSION) + '</p>' +
|
|
334
|
+
'<div class="grid">' + cards + '</div></body></html>'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function eolSafe(text) { return text }
|
|
338
|
+
|
|
339
|
+
/** 写主题对比页 + 每套主题 SVG 色板卡;默认不覆盖同名文件。 */
|
|
340
|
+
export function buildThemePreview(options = {}) {
|
|
341
|
+
const langInput = typeof options.lang === 'string' && options.lang.trim() !== '' ? options.lang.trim() : 'zh'
|
|
342
|
+
const language = resolveLanguage(langInput)
|
|
343
|
+
const outputDir = resolvePath(String(options.outputDir ?? '.'))
|
|
344
|
+
mkdirSync(outputDir, { recursive: true })
|
|
345
|
+
const overwrite = options.overwrite === true
|
|
346
|
+
const pickPath = (base, ext) => {
|
|
347
|
+
const first = resolvePath(joinPath(outputDir, base + ext))
|
|
348
|
+
if (overwrite || !existsSync(first)) return first
|
|
349
|
+
for (let i = 1; i < 1000; i += 1) {
|
|
350
|
+
const candidate = resolvePath(joinPath(outputDir, base + '-' + i + ext))
|
|
351
|
+
if (!existsSync(candidate)) return candidate
|
|
352
|
+
}
|
|
353
|
+
throw new Error('dsh-ppt:找不到可用的预览文件名(同名前缀超过 999 个),请允许 overwrite。')
|
|
354
|
+
}
|
|
355
|
+
const htmlPath = pickPath('themes-preview', '.html')
|
|
356
|
+
writeFileSync(htmlPath, renderThemePreviewHtml(language.id), 'utf8')
|
|
357
|
+
const svgs = THEME_IDS.map((id) => {
|
|
358
|
+
const svgPath = pickPath('theme-' + id, '.svg')
|
|
359
|
+
writeFileSync(svgPath, renderThemeSvg(id, language.id), 'utf8')
|
|
360
|
+
return { id, path: svgPath }
|
|
361
|
+
})
|
|
362
|
+
return { ok: true, outputDir, htmlPath, svgs, themeCount: THEME_IDS.length, language: language.id }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
export function resolveTheme(input) {
|
|
367
|
+
const id = String(input ?? DEFAULT_THEME).trim().toLowerCase()
|
|
368
|
+
const theme = THEMES[id]
|
|
369
|
+
if (!theme) {
|
|
370
|
+
throw new Error('dsh-ppt:未知主题 "' + id + '",可选:' + THEME_IDS.join(' / ') + '(默认 ' + DEFAULT_THEME + ')')
|
|
371
|
+
}
|
|
372
|
+
return theme
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function resolveLanguage(input) {
|
|
376
|
+
const id = String(input ?? 'zh').trim().toLowerCase()
|
|
377
|
+
const language = LANGUAGES[id]
|
|
378
|
+
if (!language) {
|
|
379
|
+
throw new Error('dsh-ppt:未知语言 "' + id + '",可选:zh / en / bilingual')
|
|
380
|
+
}
|
|
381
|
+
return language
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function resolveMotion(input) {
|
|
385
|
+
if (input === undefined || input === null || input === '') return true
|
|
386
|
+
if (input === true || input === 'on') return true
|
|
387
|
+
if (input === false || input === 'off') return false
|
|
388
|
+
throw new Error('dsh-ppt:未知 motion 值 "' + String(input) + '",可选:on / off(默认 on)')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function listThemes(lang = 'zh') {
|
|
392
|
+
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
393
|
+
return THEME_IDS.map((id) => {
|
|
394
|
+
const theme = THEMES[id]
|
|
395
|
+
return {
|
|
396
|
+
id,
|
|
397
|
+
name: pick(theme.name),
|
|
398
|
+
mood: pick(theme.mood),
|
|
399
|
+
bestFor: pick(theme.bestFor),
|
|
400
|
+
dark: theme.dark,
|
|
401
|
+
palette: { ...theme.palette },
|
|
402
|
+
fonts: { ...theme.fonts },
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// 文本工具
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
export function clampInt(value, fallback, min, max) {
|
|
412
|
+
const n = typeof value === 'number' ? Math.trunc(value) : fallback
|
|
413
|
+
if (!Number.isFinite(n)) return fallback
|
|
414
|
+
return Math.min(max, Math.max(min, n))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Windows 保留设备名:首个点之前的主名命中即不可用(NUL、com1、aux.report.html 都算)。
|
|
418
|
+
const WINDOWS_RESERVED_FILE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i
|
|
419
|
+
|
|
420
|
+
export function sanitizeFileName(input) {
|
|
421
|
+
const base = String(input ?? 'deck').trim().replace(/\.(html?|pptx|json)$/i, '')
|
|
422
|
+
let cleaned = base
|
|
423
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '-')
|
|
424
|
+
.replace(/\s+/g, '-')
|
|
425
|
+
.replace(/-+/g, '-')
|
|
426
|
+
.replace(/^-+|-+$/g, '')
|
|
427
|
+
if (WINDOWS_RESERVED_FILE_NAME.test(cleaned)) cleaned = 'deck-' + cleaned
|
|
428
|
+
return cleaned.slice(0, 120) || 'deck'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const DECK_ARTIFACT_EXTENSIONS = ['.html', '.pptx', '.json']
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 为三件套选择同一个可用文件名前缀。默认不覆盖:任一产物已存在时,
|
|
435
|
+
* 整组改用 -1/-2… 后缀,避免新旧 deck 被静默混写。
|
|
436
|
+
*/
|
|
437
|
+
export function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
|
|
438
|
+
// 一次 readdir 建集,替代逐个候选 existsSync 探测(最坏 ~3000 次 stat);
|
|
439
|
+
// 统一小写比较,避免 Windows 大小写不敏感文件系统把已占用的名字判成可用。
|
|
440
|
+
const existing = new Set(readdirSync(outputDir).map((name) => name.toLowerCase()))
|
|
441
|
+
const isAvailable = (candidate) => DECK_ARTIFACT_EXTENSIONS.every((ext) => !existing.has((candidate + ext).toLowerCase()))
|
|
442
|
+
if (overwrite || isAvailable(requestedFileName)) return requestedFileName
|
|
443
|
+
for (let index = 1; index < 1000; index += 1) {
|
|
444
|
+
const candidate = requestedFileName + '-' + index
|
|
445
|
+
if (isAvailable(candidate)) return candidate
|
|
446
|
+
}
|
|
447
|
+
throw new Error('dsh-ppt:找不到可用的输出文件名(同名前缀超过 999 个),请更换 fileName 或显式允许 overwrite。')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function escapeXml(value) {
|
|
451
|
+
return String(value ?? '')
|
|
452
|
+
.replace(/&/g, '&')
|
|
453
|
+
.replace(/</g, '<')
|
|
454
|
+
.replace(/>/g, '>')
|
|
455
|
+
.replace(/"/g, '"')
|
|
456
|
+
.replace(/'/g, ''')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function escapeHtml(value) {
|
|
460
|
+
return String(value ?? '')
|
|
461
|
+
.replace(/&/g, '&')
|
|
462
|
+
.replace(/</g, '<')
|
|
463
|
+
.replace(/>/g, '>')
|
|
464
|
+
.replace(/"/g, '"')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function stripInlineMarkdown(value) {
|
|
468
|
+
return String(value ?? '')
|
|
469
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
470
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
471
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
472
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
473
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
474
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
475
|
+
.replace(/_([^_\n]+)_/g, '$1')
|
|
476
|
+
.replace(/<[^>]+>/g, '')
|
|
477
|
+
.replace(/\s+/g, ' ')
|
|
478
|
+
.trim()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function splitSentences(value) {
|
|
482
|
+
const text = String(value ?? '').trim()
|
|
483
|
+
if (text === '') return []
|
|
484
|
+
const parts = text.split(/(?<=[。!?])|(?<=[.!?…])\s+/).map((part) => part.trim()).filter(Boolean)
|
|
485
|
+
return parts.length > 0 ? parts : [text]
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function truncate(value, max) {
|
|
489
|
+
const text = String(value ?? '').trim()
|
|
490
|
+
if (text.length <= max) return text
|
|
491
|
+
return text.slice(0, max - 1).replace(/\s+\S*$/, '') + '…'
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function chunk(items, size) {
|
|
495
|
+
const out = []
|
|
496
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))
|
|
497
|
+
return out
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Markdown → deck
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
function createSection(heading = '', level = 0, coverOnly = false) {
|
|
505
|
+
return { heading, level, coverOnly, bullets: [], paragraphs: [], quote: [], table: null, notes: [] }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function splitTableRow(line) {
|
|
509
|
+
const trimmed = line.trim()
|
|
510
|
+
const match = /^\|(.+)\|$/.exec(trimmed)
|
|
511
|
+
if (match === null) return null
|
|
512
|
+
return match[1].split('|').map((cell) => cell.trim())
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function isTableSeparator(cells) {
|
|
516
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{2,}:?$/.test(cell) || /^:?-+:?$/.test(cell))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const NOTES_COMMENT = /^<!--\s*(?:notes?|备注)\s*[::]\s*([\s\S]*?)\s*-->$/
|
|
520
|
+
|
|
521
|
+
function quoteSlideFrom(section, ui) {
|
|
522
|
+
const lines = [...section.quote]
|
|
523
|
+
let attribution = ''
|
|
524
|
+
const attrMatch = /^(?:——|—|--|-)\s*(.+)$/.exec(lines[lines.length - 1] ?? '')
|
|
525
|
+
if (attrMatch !== null && lines.length > 1) {
|
|
526
|
+
attribution = attrMatch[1]
|
|
527
|
+
lines.pop()
|
|
528
|
+
}
|
|
529
|
+
return { layout: 'quote', kicker: ui.quoteKicker, title: truncate(lines.join(' '), 220), subtitle: attribution }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function paginateTable(slide, continuation = '(续)') {
|
|
533
|
+
const rows = slide.rows
|
|
534
|
+
const columns = Math.max(...rows.map(row => row.length))
|
|
535
|
+
if (columns > 8) throw new Error('dsh-ppt:表格最多 8 列,请拆成多个表格后重试 / tables support up to 8 columns')
|
|
536
|
+
if (rows.some(row => row.some(cell => [...cell].length > 60))) {
|
|
537
|
+
throw new Error('dsh-ppt:表格单元格最多 60 个字符,请精简或拆分内容后重试 / cells support up to 60 characters')
|
|
538
|
+
}
|
|
539
|
+
const padded = rows.map(row => Array.from({ length: columns }, (_, i) => row[i] ?? ''))
|
|
540
|
+
const pages = chunk(padded.slice(1), 8)
|
|
541
|
+
if (pages.length === 0) pages.push([])
|
|
542
|
+
return pages.map((body, index) => {
|
|
543
|
+
const { notes, ...rest } = slide
|
|
544
|
+
const title = slide.title + (index === 0 ? '' : continuation)
|
|
545
|
+
// 同名 kicker/title 共用紧凑标题;续页不能凭空多出一行大标题挤压表格。
|
|
546
|
+
return { ...rest, title, kicker: slide.kicker === slide.title ? title : slide.kicker, rows: [padded[0], ...body],
|
|
547
|
+
...(index === 0 && notes ? { notes } : {}) }
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function tableSlidesFrom(section, ui) {
|
|
552
|
+
const table = section.table
|
|
553
|
+
const rows = [table.header, ...table.rows]
|
|
554
|
+
const heading = section.heading || ui.tableKicker
|
|
555
|
+
return paginateTable({ layout: 'table', kicker: heading, title: heading, rows }, ui.continuation)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** 把一个 section 里的金句/表格拆成独立页;备注附着到第一张产出页。 */
|
|
559
|
+
function specialSlidesFor(section, ui) {
|
|
560
|
+
const out = []
|
|
561
|
+
if (section.quote.length > 0) out.push(quoteSlideFrom(section, ui))
|
|
562
|
+
if (section.table !== null) out.push(...tableSlidesFrom(section, ui))
|
|
563
|
+
if (out.length > 0 && section.notes.length > 0) out[0].notes = section.notes.join('\n')
|
|
564
|
+
return out
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
568
|
+
const language = resolveLanguage(lang)
|
|
569
|
+
const ui = language.ui
|
|
570
|
+
const title = String(titleInput ?? '').trim()
|
|
571
|
+
let coverTitle = title
|
|
572
|
+
let coverSubtitle = ''
|
|
573
|
+
let coverSubtitleConsumed = false
|
|
574
|
+
|
|
575
|
+
let body = String(content ?? '')
|
|
576
|
+
.replace(/^\uFEFF/, '')
|
|
577
|
+
.replace(/\r\n?/g, '\n')
|
|
578
|
+
// 去除文档级 YAML frontmatter(如果有)
|
|
579
|
+
body = body.replace(/^---[ \t]*\n[\s\S]*?\n---[ \t]*\n?/, '')
|
|
580
|
+
|
|
581
|
+
const sections = []
|
|
582
|
+
let current = null
|
|
583
|
+
let firstH1Seen = false
|
|
584
|
+
|
|
585
|
+
const flush = () => {
|
|
586
|
+
if (current !== null && (current.heading !== '' || current.bullets.length > 0 || current.paragraphs.length > 0
|
|
587
|
+
|| current.table !== null || current.quote.length > 0)) {
|
|
588
|
+
sections.push(current)
|
|
589
|
+
}
|
|
590
|
+
current = null
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
for (const rawLine of body.split('\n')) {
|
|
594
|
+
const line = rawLine.trim()
|
|
595
|
+
if (line === '') {
|
|
596
|
+
// 无标题分组按空行分段,保证「文档无标题」时按段落生成幻灯片
|
|
597
|
+
if (current !== null && current.heading === '') flush()
|
|
598
|
+
continue
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line)
|
|
602
|
+
if (headingMatch !== null) {
|
|
603
|
+
const level = headingMatch[1].length
|
|
604
|
+
const heading = stripInlineMarkdown(headingMatch[2])
|
|
605
|
+
if (level === 1 && !firstH1Seen) {
|
|
606
|
+
firstH1Seen = true
|
|
607
|
+
if (coverTitle === '') coverTitle = heading
|
|
608
|
+
flush()
|
|
609
|
+
current = createSection(heading, level, true)
|
|
610
|
+
continue
|
|
611
|
+
}
|
|
612
|
+
flush()
|
|
613
|
+
current = createSection(heading, level, false)
|
|
614
|
+
continue
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const bulletMatch = /^\s*(?:[-*+]|\d+[.)])\s+(.+)$/.exec(line)
|
|
618
|
+
if (bulletMatch !== null) {
|
|
619
|
+
const bullet = stripInlineMarkdown(bulletMatch[1])
|
|
620
|
+
if (bullet !== '') {
|
|
621
|
+
if (current === null) current = createSection()
|
|
622
|
+
current.bullets.push(bullet)
|
|
623
|
+
}
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// 演讲者备注:`<!-- 备注: ... -->` / `<!-- note: ... -->` 附到当前页
|
|
628
|
+
const notesMatch = NOTES_COMMENT.exec(line)
|
|
629
|
+
if (notesMatch !== null && notesMatch[1].trim() !== '') {
|
|
630
|
+
if (current === null) current = createSection()
|
|
631
|
+
current.notes.push(notesMatch[1].trim())
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Markdown 表格:连续的 | ... | 行,第二行是分隔线
|
|
636
|
+
const tableCells = splitTableRow(line)
|
|
637
|
+
if (tableCells !== null) {
|
|
638
|
+
if (current === null) current = createSection()
|
|
639
|
+
if (isTableSeparator(tableCells)) continue
|
|
640
|
+
if (current.table === null) {
|
|
641
|
+
current.table = { header: tableCells.map((cell) => stripInlineMarkdown(cell)), rows: [] }
|
|
642
|
+
} else {
|
|
643
|
+
current.table.rows.push(tableCells.map((cell) => stripInlineMarkdown(cell)))
|
|
644
|
+
}
|
|
645
|
+
continue
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// 金句:`>` 引用块(最后一行 `—— 出处` 识别为署名)
|
|
649
|
+
const quoteMatch = /^>\s?(.*)$/.exec(line)
|
|
650
|
+
if (quoteMatch !== null) {
|
|
651
|
+
const quoted = stripInlineMarkdown(quoteMatch[1])
|
|
652
|
+
if (quoted !== '') {
|
|
653
|
+
if (current === null) current = createSection()
|
|
654
|
+
current.quote.push(quoted)
|
|
655
|
+
}
|
|
656
|
+
continue
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const paragraph = stripInlineMarkdown(line)
|
|
660
|
+
if (paragraph === '') continue
|
|
661
|
+
if (current === null) current = createSection()
|
|
662
|
+
current.paragraphs.push(paragraph)
|
|
663
|
+
}
|
|
664
|
+
flush()
|
|
665
|
+
|
|
666
|
+
const slides = []
|
|
667
|
+
const coverSource = sections.find((section) => section.coverOnly === true)
|
|
668
|
+
if (coverSource !== null && coverSource !== undefined) {
|
|
669
|
+
const coverText = coverSource.paragraphs[0] ?? coverSource.bullets[0] ?? ''
|
|
670
|
+
if (coverSubtitle === '') {
|
|
671
|
+
coverSubtitle = truncate(coverText, 180)
|
|
672
|
+
coverSubtitleConsumed = true
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
let bodySections = sections.filter((section) => section.coverOnly !== true)
|
|
677
|
+
// 首个 H1 既是封面又把所有要点收在封面节里时,把封面副标题之外的剩余要点
|
|
678
|
+
// 提升为一个无标题节,走下面的要点页生成逻辑,避免丢掉内容。
|
|
679
|
+
if (bodySections.length === 0 && coverSource !== null && coverSource !== undefined) {
|
|
680
|
+
const usedParagraph = coverSource.paragraphs.length > 0
|
|
681
|
+
const extras = [
|
|
682
|
+
...(usedParagraph ? coverSource.bullets : coverSource.bullets.slice(1)),
|
|
683
|
+
...coverSource.paragraphs.slice(usedParagraph ? 1 : 0),
|
|
684
|
+
]
|
|
685
|
+
if (extras.length > 0) {
|
|
686
|
+
const synthetic = createSection()
|
|
687
|
+
synthetic.bullets = extras
|
|
688
|
+
bodySections = [synthetic]
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 表格和引用不能只挂在封面节上,否则首个 H1 后没有 H2 时会丢失。
|
|
693
|
+
if (coverSource && (coverSource.table !== null || coverSource.quote.length > 0)) {
|
|
694
|
+
const special = createSection()
|
|
695
|
+
special.table = coverSource.table
|
|
696
|
+
special.quote = coverSource.quote
|
|
697
|
+
special.notes = coverSource.notes
|
|
698
|
+
bodySections.unshift(special)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (bodySections.length === 0) {
|
|
702
|
+
// 一句话 / 无结构输入:封面 + 核心观点 + 结束页(仍是完整的三页演示文稿)
|
|
703
|
+
const allText = coverSource !== null && coverSource !== undefined
|
|
704
|
+
? [...coverSource.bullets, ...coverSource.paragraphs].join(' ')
|
|
705
|
+
: String(content ?? '').trim()
|
|
706
|
+
if (coverSubtitle === '') coverSubtitle = truncate(splitSentences(allText)[0] ?? '', 180)
|
|
707
|
+
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
708
|
+
if (coverSubtitle !== '') {
|
|
709
|
+
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle })
|
|
710
|
+
}
|
|
711
|
+
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
712
|
+
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const hadHeadings = bodySections.some((section) => section.heading !== '')
|
|
716
|
+
|
|
717
|
+
if (!hadHeadings) {
|
|
718
|
+
// 无标题文档:第一段摘要作封面副标题,后续段落拆成要点页
|
|
719
|
+
const ordered = bodySections.map((section) => ({
|
|
720
|
+
section,
|
|
721
|
+
sentences: [...section.bullets, ...section.paragraphs].flatMap((part) => splitSentences(part)),
|
|
722
|
+
}))
|
|
723
|
+
const firstSentence = ordered[0]?.sentences[0] ?? ''
|
|
724
|
+
if (coverSubtitle === '') coverSubtitle = truncate(firstSentence, 180)
|
|
725
|
+
const bodySlides = []
|
|
726
|
+
let pointIndex = 0
|
|
727
|
+
ordered.forEach((group, groupIndex) => {
|
|
728
|
+
const specials = specialSlidesFor(group.section, ui)
|
|
729
|
+
bodySlides.push(...specials)
|
|
730
|
+
const sentences = (groupIndex === 0 && !coverSubtitleConsumed) ? group.sentences.slice(1) : group.sentences
|
|
731
|
+
for (const points of chunk(sentences, 5)) {
|
|
732
|
+
pointIndex += 1
|
|
733
|
+
const slide = {
|
|
734
|
+
layout: 'bullets',
|
|
735
|
+
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
736
|
+
title: truncate(points[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
737
|
+
bullets: points,
|
|
738
|
+
}
|
|
739
|
+
if (specials.length === 0 && group.section.notes.length > 0) slide.notes = group.section.notes.join('\n')
|
|
740
|
+
bodySlides.push(slide)
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
744
|
+
if (bodySlides.length === 0) {
|
|
745
|
+
if (coverSubtitle !== '') {
|
|
746
|
+
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle || 'Untitled' })
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
slides.push(...bodySlides)
|
|
750
|
+
}
|
|
751
|
+
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
752
|
+
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
756
|
+
let pointIndex = 0
|
|
757
|
+
for (const section of bodySections) {
|
|
758
|
+
const notes = section.notes.join('\n')
|
|
759
|
+
const specials = specialSlidesFor(section, ui)
|
|
760
|
+
slides.push(...specials)
|
|
761
|
+
if (section.heading === '') {
|
|
762
|
+
const points = [...section.bullets, ...section.paragraphs]
|
|
763
|
+
.flatMap((part) => splitSentences(part))
|
|
764
|
+
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
765
|
+
for (const [pageIndex, pagePoints] of chunk(points, 8).entries()) {
|
|
766
|
+
pointIndex += 1
|
|
767
|
+
const slide = {
|
|
768
|
+
layout: 'bullets',
|
|
769
|
+
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
770
|
+
title: truncate(pagePoints[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
771
|
+
bullets: pagePoints,
|
|
772
|
+
}
|
|
773
|
+
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
774
|
+
slides.push(slide)
|
|
775
|
+
}
|
|
776
|
+
continue
|
|
777
|
+
}
|
|
778
|
+
const points = [
|
|
779
|
+
...section.bullets,
|
|
780
|
+
...section.paragraphs.flatMap((part) => splitSentences(part)),
|
|
781
|
+
]
|
|
782
|
+
const pages = chunk(points, 8)
|
|
783
|
+
if (pages.length > 0) {
|
|
784
|
+
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
785
|
+
pages.forEach((pagePoints, pageIndex) => {
|
|
786
|
+
const label = pageIndex === 0 ? section.heading : section.heading + ui.continuation
|
|
787
|
+
const slide = { layout: 'bullets', kicker: label, title: label, bullets: pagePoints }
|
|
788
|
+
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
789
|
+
slides.push(slide)
|
|
790
|
+
})
|
|
791
|
+
} else if (specials.length === 0) {
|
|
792
|
+
const slide = { layout: 'section', kicker: section.heading, title: section.heading }
|
|
793
|
+
if (notes !== '') slide.notes = notes
|
|
794
|
+
slides.push(slide)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
798
|
+
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing', 'quote', 'table', ...MEDIA_LAYOUTS])
|
|
802
|
+
|
|
803
|
+
function normalizeSlide(raw, index) {
|
|
804
|
+
const source = (raw !== null && typeof raw === 'object') ? raw : {}
|
|
805
|
+
const layout = SLIDE_LAYOUTS.has(source.layout) ? source.layout : source.chart ? 'chart' : source.image ? 'image-right' : 'bullets'
|
|
806
|
+
const title = stripInlineMarkdown(source.title ?? '')
|
|
807
|
+
const subtitle = stripInlineMarkdown(source.subtitle ?? '')
|
|
808
|
+
const kicker = stripInlineMarkdown(source.kicker ?? '')
|
|
809
|
+
const text = stripInlineMarkdown(source.text ?? '')
|
|
810
|
+
// 备注保留换行(放映面板与备注页都按原文显示),只做转义,不做 Markdown 清洗
|
|
811
|
+
const notes = String(source.notes ?? '').trim()
|
|
812
|
+
let bullets = []
|
|
813
|
+
if (Array.isArray(source.bullets)) {
|
|
814
|
+
bullets = source.bullets.map((item) => stripInlineMarkdown(String(item))).filter(Boolean)
|
|
815
|
+
} else if (typeof source.bullets === 'string' && source.bullets.trim() !== '') {
|
|
816
|
+
bullets = splitSentences(source.bullets)
|
|
817
|
+
}
|
|
818
|
+
if (MEDIA_LAYOUTS.has(layout)) return normalizeMediaSlide(source, { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }, index)
|
|
819
|
+
if (layout === 'statement' && title === '' && text !== '') {
|
|
820
|
+
return { layout, kicker, title: text, subtitle: subtitle || '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
821
|
+
}
|
|
822
|
+
if (layout === 'quote') {
|
|
823
|
+
const quoteText = title !== '' ? title : text
|
|
824
|
+
if (quoteText === '') return { layout: 'statement', kicker, title: subtitle, subtitle: '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
825
|
+
return { layout, kicker, title: truncate(quoteText, 220), subtitle, ...(notes !== '' ? { notes } : {}) }
|
|
826
|
+
}
|
|
827
|
+
if (layout === 'table') {
|
|
828
|
+
let rows = []
|
|
829
|
+
if (Array.isArray(source.rows)) {
|
|
830
|
+
rows = source.rows
|
|
831
|
+
.map((row) => (Array.isArray(row) ? row : [row]).map((cell) => stripInlineMarkdown(String(cell))))
|
|
832
|
+
.filter((row) => row.some((cell) => cell !== ''))
|
|
833
|
+
}
|
|
834
|
+
if (rows.length === 0) {
|
|
835
|
+
// 没有表格数据的 table 页退化成要点页,避免产出空白版式
|
|
836
|
+
return { layout: 'bullets', kicker, title, subtitle, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
837
|
+
}
|
|
838
|
+
return { layout, kicker: kicker || title, title, rows, ...(notes !== '' ? { notes } : {}) }
|
|
839
|
+
}
|
|
840
|
+
return { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function checkSlideLimit(slides, limit) {
|
|
844
|
+
if (slides.length > limit) {
|
|
845
|
+
throw new Error('dsh-ppt:生成后共 ' + slides.length + ' 页,超过 maxSlides=' + limit
|
|
846
|
+
+ ',请提高 maxSlides(最多 120)或拆分内容 / generated ' + slides.length + ' slides; raise maxSlides or split the deck')
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function normalizeSlides(rawSlides, maxSlides = 60, lang = 'zh') {
|
|
851
|
+
if (!Array.isArray(rawSlides) || rawSlides.length === 0) {
|
|
852
|
+
throw new Error('dsh-ppt:slides 必须是非空数组(每个元素是 { layout, title, subtitle, kicker, bullets } 对象)')
|
|
853
|
+
}
|
|
854
|
+
const limit = clampInt(maxSlides, 60, 1, 120)
|
|
855
|
+
const ui = resolveLanguage(lang).ui
|
|
856
|
+
const slides = rawSlides.map(normalizeSlide).flatMap(slide => slide.layout === 'table'
|
|
857
|
+
? paginateTable(slide, ui.continuation) : [slide])
|
|
858
|
+
checkSlideLimit(slides, limit)
|
|
859
|
+
if (slides.length === 0) throw new Error('dsh-ppt:slides 规范化后为空')
|
|
860
|
+
return slides
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ---------------------------------------------------------------------------
|
|
864
|
+
// 构建入口
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
|
|
867
|
+
export function normalizeBuildOptions(options = {}) {
|
|
868
|
+
const title = String(options.title ?? '').trim()
|
|
869
|
+
if (title === '') throw new Error('dsh-ppt:title 不能为空')
|
|
870
|
+
const theme = resolveTheme(options.theme)
|
|
871
|
+
const language = resolveLanguage(options.lang)
|
|
872
|
+
const motion = resolveMotion(options.motion)
|
|
873
|
+
const maxSlides = clampInt(options.maxSlides, 60, 3, 120)
|
|
874
|
+
let deck
|
|
875
|
+
if (Array.isArray(options.slides) && options.slides.length > 0) {
|
|
876
|
+
deck = {
|
|
877
|
+
title,
|
|
878
|
+
subtitle: stripInlineMarkdown(options.subtitle ?? ''),
|
|
879
|
+
slides: normalizeSlides(options.slides, maxSlides, language.id),
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
const content = String(options.content ?? '').trim()
|
|
883
|
+
if (content === '') throw new Error('dsh-ppt:content 不能为空(或用 slides 传结构化幻灯片)')
|
|
884
|
+
deck = parseMarkdownDeck(title, content, language.id)
|
|
885
|
+
checkSlideLimit(deck.slides, maxSlides)
|
|
886
|
+
}
|
|
887
|
+
if (deck.slides.length < 1) throw new Error('dsh-ppt:没有可生成的幻灯片')
|
|
888
|
+
const outputDir = resolvePath(String(options.outputDir ?? '.').trim() || '.')
|
|
889
|
+
const fileName = sanitizeFileName(options.fileName ?? title)
|
|
890
|
+
const overwrite = options.overwrite === true
|
|
891
|
+
return { title, theme, language, motion, deck, outputDir, fileName, overwrite }
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* 三件套落盘:三份内容全部先写同目录临时文件,再逐个 rename 提交。
|
|
896
|
+
* 任一步失败都清理临时文件,并把已提交的目标恢复为调用前的内容,
|
|
897
|
+
* 避免出现「json/html 已更新、pptx 还是旧的」这类混版状态。
|
|
898
|
+
*/
|
|
899
|
+
export function commitDeckArtifacts(artifacts) {
|
|
900
|
+
const stamp = process.pid.toString(36) + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8)
|
|
901
|
+
const items = artifacts.map((item, index) => ({ ...item, tempPath: item.path + '.' + stamp + '-' + index + '.tmp' }))
|
|
902
|
+
const removeTemp = (item) => {
|
|
903
|
+
try {
|
|
904
|
+
rmSync(item.tempPath, { force: true })
|
|
905
|
+
} catch {}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// 阶段 0:记下已有产物内容用于回滚;读不出来(如被同名目录占用)先报错,不碰任何文件。
|
|
909
|
+
const previous = new Map()
|
|
910
|
+
for (const item of items) {
|
|
911
|
+
if (!existsSync(item.path)) continue
|
|
912
|
+
try {
|
|
913
|
+
previous.set(item.path, readFileSync(item.path))
|
|
914
|
+
} catch (error) {
|
|
915
|
+
throw new Error('dsh-ppt:三件套写入失败(' + item.path + ' 已存在但无法读取),原文件未改动:' + error.message)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 阶段 1:全部写临时文件;这一步失败时目标文件尚未被改动。
|
|
920
|
+
let written = 0
|
|
921
|
+
try {
|
|
922
|
+
for (const item of items) {
|
|
923
|
+
writeFileSync(item.tempPath, item.data, item.encoding)
|
|
924
|
+
written += 1
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
for (const item of items.slice(0, written + 1)) removeTemp(item)
|
|
928
|
+
throw new Error('dsh-ppt:写入临时文件失败(' + items[written].path + '),原三件套未改动:' + error.message)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// 阶段 2:逐个 rename 提交;中途失败则把已提交的目标回滚成旧内容。
|
|
932
|
+
const committed = []
|
|
933
|
+
try {
|
|
934
|
+
for (const item of items) {
|
|
935
|
+
renameSync(item.tempPath, item.path)
|
|
936
|
+
committed.push(item)
|
|
937
|
+
}
|
|
938
|
+
} catch (error) {
|
|
939
|
+
const failedPath = items[committed.length].path
|
|
940
|
+
for (const item of items) removeTemp(item)
|
|
941
|
+
for (const item of committed) {
|
|
942
|
+
try {
|
|
943
|
+
const previousData = previous.get(item.path)
|
|
944
|
+
if (previousData === undefined) rmSync(item.path, { force: true })
|
|
945
|
+
else writeFileSync(item.path, previousData)
|
|
946
|
+
} catch {}
|
|
947
|
+
}
|
|
948
|
+
throw new Error('dsh-ppt:提交三件套失败(' + failedPath + '),已回滚为原文件:' + error.message)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export function buildDeck(options = {}) {
|
|
953
|
+
if (options.brand || options.template || options.slides?.some(slide => MEDIA_LAYOUTS.has(slide.layout) || slide.image || slide.chart)) {
|
|
954
|
+
throw new Error('dsh-ppt:图片、图表、模板与品牌请使用异步 buildDeckAsync 或 ppt_create / CLI')
|
|
955
|
+
}
|
|
956
|
+
const normalized = normalizeBuildOptions(options)
|
|
957
|
+
const { title, theme, language, motion, deck, outputDir, overwrite } = normalized
|
|
958
|
+
mkdirSync(outputDir, { recursive: true })
|
|
959
|
+
const fileName = resolveDeckFileName(outputDir, normalized.fileName, overwrite)
|
|
960
|
+
|
|
961
|
+
const manifest = {
|
|
962
|
+
version: DECK_VERSION,
|
|
963
|
+
title,
|
|
964
|
+
theme: theme.id,
|
|
965
|
+
language: language.id,
|
|
966
|
+
motion,
|
|
967
|
+
slideCount: deck.slides.length,
|
|
968
|
+
slides: deck.slides,
|
|
969
|
+
}
|
|
970
|
+
const jsonPath = resolvePath(outputDir, fileName + '.json')
|
|
971
|
+
const htmlPath = resolvePath(outputDir, fileName + '.html')
|
|
972
|
+
const pptxPath = resolvePath(outputDir, fileName + '.pptx')
|
|
973
|
+
|
|
974
|
+
// 先生成三份内容再统一提交:生成阶段抛错时也不会留下半套文件。
|
|
975
|
+
commitDeckArtifacts([
|
|
976
|
+
{ path: jsonPath, data: JSON.stringify(manifest, null, 2) + '\n', encoding: 'utf8' },
|
|
977
977
|
{ path: htmlPath, data: renderHtml(manifest, theme, language, { pptx: pptxPath, json: jsonPath }), encoding: 'utf8' },
|
|
978
|
-
{ path: pptxPath, data: buildPptx(manifest, theme, language) },
|
|
979
|
-
])
|
|
980
|
-
|
|
981
|
-
return {
|
|
982
|
-
ok: true,
|
|
983
|
-
title,
|
|
984
|
-
theme: theme.id,
|
|
985
|
-
language: language.id,
|
|
986
|
-
slideCount: deck.slides.length,
|
|
987
|
-
outputDir,
|
|
988
|
-
files: {
|
|
989
|
-
html: htmlPath,
|
|
990
|
-
pptx: pptxPath,
|
|
991
|
-
json: jsonPath,
|
|
992
|
-
},
|
|
993
|
-
htmlPath,
|
|
994
|
-
pptxPath,
|
|
995
|
-
jsonPath,
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// ---------------------------------------------------------------------------
|
|
1000
|
-
// HTML 网页放映
|
|
1001
|
-
// ---------------------------------------------------------------------------
|
|
1002
|
-
|
|
978
|
+
{ path: pptxPath, data: buildPptx(manifest, theme, language) },
|
|
979
|
+
])
|
|
980
|
+
|
|
981
|
+
return {
|
|
982
|
+
ok: true,
|
|
983
|
+
title,
|
|
984
|
+
theme: theme.id,
|
|
985
|
+
language: language.id,
|
|
986
|
+
slideCount: deck.slides.length,
|
|
987
|
+
outputDir,
|
|
988
|
+
files: {
|
|
989
|
+
html: htmlPath,
|
|
990
|
+
pptx: pptxPath,
|
|
991
|
+
json: jsonPath,
|
|
992
|
+
},
|
|
993
|
+
htmlPath,
|
|
994
|
+
pptxPath,
|
|
995
|
+
jsonPath,
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// ---------------------------------------------------------------------------
|
|
1000
|
+
// HTML 网页放映
|
|
1001
|
+
// ---------------------------------------------------------------------------
|
|
1002
|
+
|
|
1003
1003
|
export function renderHtml(manifest, theme, language, files = {}) {
|
|
1004
|
-
const t = theme
|
|
1005
|
-
const lang = language
|
|
1006
|
-
const ui = lang.ui
|
|
1007
|
-
const motion = manifest.motion !== false
|
|
1008
|
-
const motionCss = motion
|
|
1009
|
-
? `body.motion .slide.is-active{animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
1010
|
-
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
1011
|
-
body.motion .bullets li{
|
|
1012
|
-
opacity:0;animation:bullet-in .55s cubic-bezier(.22,.8,.36,1) forwards;
|
|
1013
|
-
animation-delay:calc(var(--i, 0)*90ms + .12s);
|
|
1014
|
-
}
|
|
1015
|
-
@keyframes bullet-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}`
|
|
1016
|
-
: ''
|
|
1017
|
-
const slides = manifest.slides.map((slide, index) => {
|
|
1018
|
-
const label = slide.title || slide.kicker || (ui.slide + ' ' + (index + 1))
|
|
1019
|
-
return '<div class="slide-frame" data-frame="' + (index + 1) + '" tabindex="-1" role="button" aria-label="' + escapeHtml(String(label)) + '">' +
|
|
1020
|
-
renderHtmlSlide(slide, index, ui, lang.id, manifest, theme) + '</div>'
|
|
1021
|
-
}).join('\n')
|
|
1022
|
-
const themeLabel = t.name[lang.id] ?? t.name.en
|
|
1004
|
+
const t = theme
|
|
1005
|
+
const lang = language
|
|
1006
|
+
const ui = lang.ui
|
|
1007
|
+
const motion = manifest.motion !== false
|
|
1008
|
+
const motionCss = motion
|
|
1009
|
+
? `body.motion .slide.is-active{animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
1010
|
+
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
1011
|
+
body.motion .bullets li{
|
|
1012
|
+
opacity:0;animation:bullet-in .55s cubic-bezier(.22,.8,.36,1) forwards;
|
|
1013
|
+
animation-delay:calc(var(--i, 0)*90ms + .12s);
|
|
1014
|
+
}
|
|
1015
|
+
@keyframes bullet-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}`
|
|
1016
|
+
: ''
|
|
1017
|
+
const slides = manifest.slides.map((slide, index) => {
|
|
1018
|
+
const label = slide.title || slide.kicker || (ui.slide + ' ' + (index + 1))
|
|
1019
|
+
return '<div class="slide-frame" data-frame="' + (index + 1) + '" tabindex="-1" role="button" aria-label="' + escapeHtml(String(label)) + '">' +
|
|
1020
|
+
renderHtmlSlide(slide, index, ui, lang.id, manifest, theme) + '</div>'
|
|
1021
|
+
}).join('\n')
|
|
1022
|
+
const themeLabel = t.name[lang.id] ?? t.name.en
|
|
1023
1023
|
const total = manifest.slides.length
|
|
1024
1024
|
const delivery = renderDelivery(manifest, files, lang.id)
|
|
1025
|
-
|
|
1026
|
-
return `<!DOCTYPE html>
|
|
1027
|
-
<html lang="${lang.attr}">
|
|
1028
|
-
<head>
|
|
1029
|
-
<meta charset="UTF-8">
|
|
1030
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1031
|
-
<link rel="icon" href="data:,">
|
|
1032
|
-
<title>${escapeHtml(manifest.title)}</title>
|
|
1033
|
-
<style>
|
|
1034
|
-
:root{
|
|
1035
|
-
--bg:${t.palette.bg};
|
|
1036
|
-
--panel:${t.palette.panel};
|
|
1037
|
-
--fg:${t.palette.fg};
|
|
1038
|
-
--muted:${t.palette.muted};
|
|
1039
|
-
--accent:${t.palette.accent};
|
|
1040
|
-
--accent2:${t.palette.accent2};
|
|
1041
|
-
--font-heading:${t.fonts.heading};
|
|
1042
|
-
--font-body:${t.fonts.body};
|
|
1043
|
-
}
|
|
1044
|
-
*{box-sizing:border-box}
|
|
1045
|
-
html,body{height:100%}
|
|
1046
|
-
body{
|
|
1047
|
-
margin:0;background:var(--bg);color:var(--fg);
|
|
1048
|
-
font-family:var(--font-body);overflow:hidden;
|
|
1049
|
-
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
|
|
1050
|
-
}
|
|
1051
|
-
#stage{position:fixed;inset:0}
|
|
1052
|
-
.slide-frame{position:fixed;inset:0}
|
|
1053
|
-
.slide{
|
|
1054
|
-
position:absolute;inset:0;display:none;flex-direction:column;justify-content:center;
|
|
1055
|
-
padding:clamp(34px,7vw,110px);overflow:hidden;
|
|
1056
|
-
}
|
|
1057
|
-
.slide::before{
|
|
1058
|
-
content:"";position:absolute;inset:-20%;pointer-events:none;z-index:-1;
|
|
1059
|
-
background:
|
|
1060
|
-
radial-gradient(42% 34% at 82% 18%, ${hexToRgba(t.palette.accent, t.dark ? 0.22 : 0.12)}, transparent 70%),
|
|
1061
|
-
radial-gradient(36% 30% at 12% 86%, ${hexToRgba(t.palette.accent2, t.dark ? 0.16 : 0.12)}, transparent 70%),
|
|
1062
|
-
radial-gradient(70% 60% at 50% 50%, ${hexToRgba(t.palette.panel, 0.55)}, transparent 100%);
|
|
1063
|
-
}
|
|
1064
|
-
.slide.is-active{display:flex}
|
|
1065
|
-
${motionCss}
|
|
1066
|
-
${MEDIA_CSS}
|
|
1067
|
-
.kicker{
|
|
1068
|
-
color:var(--accent);font-weight:700;letter-spacing:.18em;text-transform:uppercase;
|
|
1069
|
-
font-size:clamp(12px,1.3vw,18px);margin-bottom:22px;
|
|
1070
|
-
}
|
|
1071
|
-
h1,h2,.statement-title{font-family:var(--font-heading);line-height:1.06;letter-spacing:-.015em;margin:0}
|
|
1072
|
-
h1{font-size:clamp(44px,7.4vw,118px);max-width:20ch}
|
|
1073
|
-
h2{font-size:clamp(34px,5vw,82px);max-width:20ch}
|
|
1074
|
-
.subtitle{
|
|
1075
|
-
color:var(--muted);font-size:clamp(18px,2.3vw,34px);line-height:1.5;
|
|
1076
|
-
max-width:46em;margin-top:28px;
|
|
1077
|
-
}
|
|
1078
|
-
.meta{
|
|
1079
|
-
color:var(--muted);font-size:clamp(12px,1.2vw,16px);margin-top:48px;
|
|
1080
|
-
letter-spacing:.06em;
|
|
1081
|
-
}
|
|
1082
|
-
.bullets ul{margin:30px 0 0;padding:0;list-style:none;display:grid;gap:clamp(12px,1.6vw,24px)}
|
|
1083
|
-
.bullets li{
|
|
1084
|
-
position:relative;padding-left:clamp(28px,2.6vw,44px);
|
|
1085
|
-
font-size:clamp(20px,2.6vw,40px);line-height:1.35;max-width:24em;
|
|
1086
|
-
}
|
|
1087
|
-
.bullets li::before{
|
|
1088
|
-
content:"";position:absolute;left:0;top:.58em;width:.5em;height:.5em;
|
|
1089
|
-
background:var(--accent);border-radius:2px;box-shadow:.28em .28em 0 color-mix(in srgb, var(--accent2) 78%, transparent);
|
|
1090
|
-
}
|
|
1091
|
-
.section .kicker{margin-bottom:10px}
|
|
1092
|
-
.section .accent-line{width:min(180px,18vw);height:6px;background:var(--accent);margin:28px 0}
|
|
1093
|
-
.statement-title{
|
|
1094
|
-
font-size:clamp(34px,5.4vw,88px);max-width:22ch;font-weight:800;
|
|
1095
|
-
border-left:6px solid var(--accent);padding-left:clamp(22px,3vw,48px);
|
|
1096
|
-
}
|
|
1097
|
-
.closing{text-align:center;align-items:center}
|
|
1098
|
-
.closing h1,.closing .statement-title{font-size:clamp(52px,9vw,148px)}
|
|
1099
|
-
.closing .subtitle{color:var(--accent);font-weight:700}
|
|
1100
|
-
.cover h1{font-weight:900}
|
|
1101
|
-
.quote-mark{
|
|
1102
|
-
font-family:var(--font-heading);font-size:clamp(90px,13vw,210px);line-height:.62;
|
|
1103
|
-
color:var(--accent);margin-bottom:clamp(14px,2vw,30px);
|
|
1104
|
-
}
|
|
1105
|
-
.quote-text{
|
|
1106
|
-
font-family:var(--font-heading);font-size:clamp(26px,4.2vw,62px);line-height:1.28;
|
|
1107
|
-
font-weight:700;max-width:22ch;margin:0;
|
|
1108
|
-
}
|
|
1109
|
-
.quote-attr{color:var(--muted);margin-top:clamp(18px,2.6vw,34px);font-size:clamp(15px,1.8vw,26px);letter-spacing:.04em}
|
|
1110
|
-
table.deck-table{
|
|
1111
|
-
border-collapse:collapse;margin-top:clamp(20px,3vw,42px);width:100%;max-width:100%;
|
|
1112
|
-
font-size:clamp(14px,1.55vw,23px);
|
|
1113
|
-
}
|
|
1114
|
-
.deck-table th{
|
|
1115
|
-
background:color-mix(in srgb, var(--accent) 26%, var(--bg));color:var(--fg);
|
|
1116
|
-
text-align:left;font-weight:700;padding:.6em .9em;letter-spacing:.02em;
|
|
1117
|
-
border-bottom:2px solid var(--accent);
|
|
1118
|
-
}
|
|
1119
|
-
.deck-table td{
|
|
1120
|
-
padding:.55em .9em;border-bottom:1px solid color-mix(in srgb, var(--muted) 55%, transparent);
|
|
1121
|
-
color:var(--fg);
|
|
1122
|
-
}
|
|
1123
|
-
.deck-table .num{text-align:right;font-variant-numeric:tabular-nums}
|
|
1124
|
-
.deck-table tbody tr:nth-child(even){background:color-mix(in srgb, var(--panel) 62%, transparent)}
|
|
1125
|
-
#progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);z-index:30;transition:width .25s}
|
|
1126
|
-
#hud{
|
|
1025
|
+
|
|
1026
|
+
return `<!DOCTYPE html>
|
|
1027
|
+
<html lang="${lang.attr}">
|
|
1028
|
+
<head>
|
|
1029
|
+
<meta charset="UTF-8">
|
|
1030
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1031
|
+
<link rel="icon" href="data:,">
|
|
1032
|
+
<title>${escapeHtml(manifest.title)}</title>
|
|
1033
|
+
<style>
|
|
1034
|
+
:root{
|
|
1035
|
+
--bg:${t.palette.bg};
|
|
1036
|
+
--panel:${t.palette.panel};
|
|
1037
|
+
--fg:${t.palette.fg};
|
|
1038
|
+
--muted:${t.palette.muted};
|
|
1039
|
+
--accent:${t.palette.accent};
|
|
1040
|
+
--accent2:${t.palette.accent2};
|
|
1041
|
+
--font-heading:${t.fonts.heading};
|
|
1042
|
+
--font-body:${t.fonts.body};
|
|
1043
|
+
}
|
|
1044
|
+
*{box-sizing:border-box}
|
|
1045
|
+
html,body{height:100%}
|
|
1046
|
+
body{
|
|
1047
|
+
margin:0;background:var(--bg);color:var(--fg);
|
|
1048
|
+
font-family:var(--font-body);overflow:hidden;
|
|
1049
|
+
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
|
|
1050
|
+
}
|
|
1051
|
+
#stage{position:fixed;inset:0}
|
|
1052
|
+
.slide-frame{position:fixed;inset:0}
|
|
1053
|
+
.slide{
|
|
1054
|
+
position:absolute;inset:0;display:none;flex-direction:column;justify-content:center;
|
|
1055
|
+
padding:clamp(34px,7vw,110px);overflow:hidden;
|
|
1056
|
+
}
|
|
1057
|
+
.slide::before{
|
|
1058
|
+
content:"";position:absolute;inset:-20%;pointer-events:none;z-index:-1;
|
|
1059
|
+
background:
|
|
1060
|
+
radial-gradient(42% 34% at 82% 18%, ${hexToRgba(t.palette.accent, t.dark ? 0.22 : 0.12)}, transparent 70%),
|
|
1061
|
+
radial-gradient(36% 30% at 12% 86%, ${hexToRgba(t.palette.accent2, t.dark ? 0.16 : 0.12)}, transparent 70%),
|
|
1062
|
+
radial-gradient(70% 60% at 50% 50%, ${hexToRgba(t.palette.panel, 0.55)}, transparent 100%);
|
|
1063
|
+
}
|
|
1064
|
+
.slide.is-active{display:flex}
|
|
1065
|
+
${motionCss}
|
|
1066
|
+
${MEDIA_CSS}
|
|
1067
|
+
.kicker{
|
|
1068
|
+
color:var(--accent);font-weight:700;letter-spacing:.18em;text-transform:uppercase;
|
|
1069
|
+
font-size:clamp(12px,1.3vw,18px);margin-bottom:22px;
|
|
1070
|
+
}
|
|
1071
|
+
h1,h2,.statement-title{font-family:var(--font-heading);line-height:1.06;letter-spacing:-.015em;margin:0}
|
|
1072
|
+
h1{font-size:clamp(44px,7.4vw,118px);max-width:20ch}
|
|
1073
|
+
h2{font-size:clamp(34px,5vw,82px);max-width:20ch}
|
|
1074
|
+
.subtitle{
|
|
1075
|
+
color:var(--muted);font-size:clamp(18px,2.3vw,34px);line-height:1.5;
|
|
1076
|
+
max-width:46em;margin-top:28px;
|
|
1077
|
+
}
|
|
1078
|
+
.meta{
|
|
1079
|
+
color:var(--muted);font-size:clamp(12px,1.2vw,16px);margin-top:48px;
|
|
1080
|
+
letter-spacing:.06em;
|
|
1081
|
+
}
|
|
1082
|
+
.bullets ul{margin:30px 0 0;padding:0;list-style:none;display:grid;gap:clamp(12px,1.6vw,24px)}
|
|
1083
|
+
.bullets li{
|
|
1084
|
+
position:relative;padding-left:clamp(28px,2.6vw,44px);
|
|
1085
|
+
font-size:clamp(20px,2.6vw,40px);line-height:1.35;max-width:24em;
|
|
1086
|
+
}
|
|
1087
|
+
.bullets li::before{
|
|
1088
|
+
content:"";position:absolute;left:0;top:.58em;width:.5em;height:.5em;
|
|
1089
|
+
background:var(--accent);border-radius:2px;box-shadow:.28em .28em 0 color-mix(in srgb, var(--accent2) 78%, transparent);
|
|
1090
|
+
}
|
|
1091
|
+
.section .kicker{margin-bottom:10px}
|
|
1092
|
+
.section .accent-line{width:min(180px,18vw);height:6px;background:var(--accent);margin:28px 0}
|
|
1093
|
+
.statement-title{
|
|
1094
|
+
font-size:clamp(34px,5.4vw,88px);max-width:22ch;font-weight:800;
|
|
1095
|
+
border-left:6px solid var(--accent);padding-left:clamp(22px,3vw,48px);
|
|
1096
|
+
}
|
|
1097
|
+
.closing{text-align:center;align-items:center}
|
|
1098
|
+
.closing h1,.closing .statement-title{font-size:clamp(52px,9vw,148px)}
|
|
1099
|
+
.closing .subtitle{color:var(--accent);font-weight:700}
|
|
1100
|
+
.cover h1{font-weight:900}
|
|
1101
|
+
.quote-mark{
|
|
1102
|
+
font-family:var(--font-heading);font-size:clamp(90px,13vw,210px);line-height:.62;
|
|
1103
|
+
color:var(--accent);margin-bottom:clamp(14px,2vw,30px);
|
|
1104
|
+
}
|
|
1105
|
+
.quote-text{
|
|
1106
|
+
font-family:var(--font-heading);font-size:clamp(26px,4.2vw,62px);line-height:1.28;
|
|
1107
|
+
font-weight:700;max-width:22ch;margin:0;
|
|
1108
|
+
}
|
|
1109
|
+
.quote-attr{color:var(--muted);margin-top:clamp(18px,2.6vw,34px);font-size:clamp(15px,1.8vw,26px);letter-spacing:.04em}
|
|
1110
|
+
table.deck-table{
|
|
1111
|
+
border-collapse:collapse;margin-top:clamp(20px,3vw,42px);width:100%;max-width:100%;
|
|
1112
|
+
font-size:clamp(14px,1.55vw,23px);
|
|
1113
|
+
}
|
|
1114
|
+
.deck-table th{
|
|
1115
|
+
background:color-mix(in srgb, var(--accent) 26%, var(--bg));color:var(--fg);
|
|
1116
|
+
text-align:left;font-weight:700;padding:.6em .9em;letter-spacing:.02em;
|
|
1117
|
+
border-bottom:2px solid var(--accent);
|
|
1118
|
+
}
|
|
1119
|
+
.deck-table td{
|
|
1120
|
+
padding:.55em .9em;border-bottom:1px solid color-mix(in srgb, var(--muted) 55%, transparent);
|
|
1121
|
+
color:var(--fg);
|
|
1122
|
+
}
|
|
1123
|
+
.deck-table .num{text-align:right;font-variant-numeric:tabular-nums}
|
|
1124
|
+
.deck-table tbody tr:nth-child(even){background:color-mix(in srgb, var(--panel) 62%, transparent)}
|
|
1125
|
+
#progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);z-index:30;transition:width .25s}
|
|
1126
|
+
#hud{
|
|
1127
1127
|
position:fixed;left:22px;right:22px;bottom:18px;z-index:50;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;align-items:center;
|
|
1128
|
-
color:var(--muted);font-size:13px;letter-spacing:.08em;font-variant-numeric:tabular-nums;
|
|
1129
|
-
}
|
|
1130
|
-
#hud button{
|
|
1131
|
-
background:color-mix(in srgb, var(--panel) 88%, transparent);color:var(--fg);
|
|
1132
|
-
border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;
|
|
1133
|
-
padding:8px 14px;min-height:34px;font:inherit;cursor:pointer;
|
|
1134
|
-
}
|
|
1135
|
-
#hud button:hover{border-color:var(--accent);color:var(--accent)}
|
|
1136
|
-
#notes-panel{
|
|
1137
|
-
position:fixed;left:0;right:0;bottom:64px;z-index:40;display:none;
|
|
1138
|
-
background:color-mix(in srgb, var(--panel) 96%, black 4%);
|
|
1139
|
-
border-top:2px solid var(--accent);
|
|
1140
|
-
padding:14px clamp(22px,4vw,64px) 18px;max-height:38vh;overflow:auto;
|
|
1141
|
-
}
|
|
1142
|
-
body.notes-open #notes-panel{display:block}
|
|
1143
|
-
#notes-panel .notes-head{display:flex;align-items:center;justify-content:space-between;gap:16px}
|
|
1144
|
-
#notes-close{
|
|
1145
|
-
background:transparent;color:var(--muted);border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);
|
|
1146
|
-
border-radius:99px;padding:4px 12px;font:inherit;cursor:pointer;min-height:30px;
|
|
1147
|
-
}
|
|
1148
|
-
#notes-close:hover{color:var(--accent);border-color:var(--accent)}
|
|
1149
|
-
#notes-panel .notes-label{
|
|
1150
|
-
color:var(--accent);font-weight:700;letter-spacing:.14em;text-transform:uppercase;
|
|
1151
|
-
font-size:12px;margin-bottom:8px;
|
|
1152
|
-
}
|
|
1153
|
-
#notes-text{white-space:pre-wrap;line-height:1.65;font-size:clamp(15px,1.5vw,18px);color:var(--fg)}
|
|
1154
|
-
#notes-text.is-empty{color:var(--muted);font-style:italic}
|
|
1155
|
-
#help-panel{
|
|
1156
|
-
position:fixed;right:22px;bottom:64px;z-index:60;max-width:390px;
|
|
1157
|
-
background:color-mix(in srgb, var(--panel) 96%, black 4%);color:var(--fg);
|
|
1158
|
-
border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);border-radius:14px;
|
|
1159
|
-
padding:16px 20px;box-shadow:0 18px 50px rgba(0,0,0,.45);font-size:14px;line-height:1.8;
|
|
1160
|
-
}
|
|
1161
|
-
#help-panel[hidden]{display:none}
|
|
1162
|
-
#help-panel kbd{
|
|
1163
|
-
background:color-mix(in srgb, var(--accent) 22%, transparent);border-radius:5px;
|
|
1164
|
-
padding:1px 6px;font-family:inherit;font-size:12px;
|
|
1165
|
-
}
|
|
1166
|
-
#toast{
|
|
1167
|
-
position:fixed;left:50%;bottom:84px;transform:translateX(-50%);z-index:70;
|
|
1168
|
-
background:color-mix(in srgb, var(--panel) 96%, black 4%);color:var(--fg);
|
|
1169
|
-
border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);border-radius:99px;
|
|
1170
|
-
padding:9px 18px;font-size:13px;
|
|
1171
|
-
}
|
|
1172
|
-
#toast[hidden]{display:none}
|
|
1173
|
-
#blank{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
|
|
1174
|
-
body.blank-black #blank{display:block;background:#000}
|
|
1175
|
-
body.blank-white #blank{display:block;background:#fff}
|
|
1176
|
-
body.blank-black #hud,body.blank-white #hud{visibility:hidden;pointer-events:none}
|
|
1177
|
-
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
1178
|
-
body.overview #stage{
|
|
1179
|
-
display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px;
|
|
1180
|
-
grid-auto-rows:var(--deck-thumb-height,180px);
|
|
1181
|
-
padding:18px 18px 92px;position:fixed;inset:0;overflow:auto;align-content:start;
|
|
1182
|
-
}
|
|
1183
|
-
body.overview .slide-frame{
|
|
1184
|
-
position:relative;inset:auto;width:100%;aspect-ratio:16/9;overflow:hidden;
|
|
1185
|
-
border:1px solid color-mix(in srgb, var(--muted) 35%, transparent);border-radius:12px;
|
|
1186
|
-
cursor:pointer;background:var(--bg);outline:none;
|
|
1187
|
-
}
|
|
1188
|
-
body.overview .slide-frame:hover{border-color:var(--accent)}
|
|
1189
|
-
body.overview .slide-frame.is-current{outline:2px solid var(--accent);outline-offset:2px}
|
|
1190
|
-
body.overview .slide-frame:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
|
|
1191
|
-
body.overview .slide-frame::after{
|
|
1192
|
-
content:attr(data-frame);position:absolute;right:8px;bottom:6px;z-index:2;
|
|
1193
|
-
font-size:12px;color:var(--muted);background:color-mix(in srgb, var(--bg) 84%, transparent);
|
|
1194
|
-
border:1px solid color-mix(in srgb, var(--muted) 40%, transparent);border-radius:6px;
|
|
1195
|
-
padding:1px 7px;font-variant-numeric:tabular-nums;
|
|
1196
|
-
}
|
|
1197
|
-
body.overview .slide{
|
|
1198
|
-
display:flex !important;position:absolute;inset:0;width:1280px;height:720px;
|
|
1199
|
-
transform:scale(var(--deck-thumb-scale,.25));transform-origin:top left;pointer-events:none;
|
|
1200
|
-
animation:none !important;
|
|
1201
|
-
}
|
|
1202
|
-
body.overview .bullets li{opacity:1 !important;animation:none !important}
|
|
1203
|
-
body.overview .media-canvas{width:1280px;height:720px}
|
|
1204
|
-
body.overview #notes-panel,body.presenting #notes-panel{display:none !important}
|
|
1205
|
-
body.presenting #notes-toggle{opacity:.55}
|
|
1206
|
-
body.presenting #hud{opacity:0;pointer-events:none;transition:opacity .25s}
|
|
1207
|
-
body.presenting #hud:hover{opacity:1;pointer-events:auto}
|
|
1208
|
-
@media (max-width:900px){
|
|
1209
|
-
.slide{padding:clamp(22px,5vw,44px)}
|
|
1210
|
-
h1{font-size:clamp(34px,8.5vw,64px)}
|
|
1211
|
-
h2{font-size:clamp(26px,6.5vw,48px)}
|
|
1212
|
-
.statement-title{font-size:clamp(26px,7vw,54px)}
|
|
1213
|
-
.subtitle{font-size:clamp(16px,3.4vw,24px)}
|
|
1214
|
-
.bullets li{font-size:clamp(16px,3.4vw,24px)}
|
|
1215
|
-
.kicker{font-size:13px}
|
|
1216
|
-
.deck-table{font-size:14px}
|
|
1217
|
-
.deck-table th,.deck-table td{padding:.55em .6em}
|
|
1128
|
+
color:var(--muted);font-size:13px;letter-spacing:.08em;font-variant-numeric:tabular-nums;
|
|
1129
|
+
}
|
|
1130
|
+
#hud button{
|
|
1131
|
+
background:color-mix(in srgb, var(--panel) 88%, transparent);color:var(--fg);
|
|
1132
|
+
border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;
|
|
1133
|
+
padding:8px 14px;min-height:34px;font:inherit;cursor:pointer;
|
|
1134
|
+
}
|
|
1135
|
+
#hud button:hover{border-color:var(--accent);color:var(--accent)}
|
|
1136
|
+
#notes-panel{
|
|
1137
|
+
position:fixed;left:0;right:0;bottom:64px;z-index:40;display:none;
|
|
1138
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);
|
|
1139
|
+
border-top:2px solid var(--accent);
|
|
1140
|
+
padding:14px clamp(22px,4vw,64px) 18px;max-height:38vh;overflow:auto;
|
|
1141
|
+
}
|
|
1142
|
+
body.notes-open #notes-panel{display:block}
|
|
1143
|
+
#notes-panel .notes-head{display:flex;align-items:center;justify-content:space-between;gap:16px}
|
|
1144
|
+
#notes-close{
|
|
1145
|
+
background:transparent;color:var(--muted);border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);
|
|
1146
|
+
border-radius:99px;padding:4px 12px;font:inherit;cursor:pointer;min-height:30px;
|
|
1147
|
+
}
|
|
1148
|
+
#notes-close:hover{color:var(--accent);border-color:var(--accent)}
|
|
1149
|
+
#notes-panel .notes-label{
|
|
1150
|
+
color:var(--accent);font-weight:700;letter-spacing:.14em;text-transform:uppercase;
|
|
1151
|
+
font-size:12px;margin-bottom:8px;
|
|
1152
|
+
}
|
|
1153
|
+
#notes-text{white-space:pre-wrap;line-height:1.65;font-size:clamp(15px,1.5vw,18px);color:var(--fg)}
|
|
1154
|
+
#notes-text.is-empty{color:var(--muted);font-style:italic}
|
|
1155
|
+
#help-panel{
|
|
1156
|
+
position:fixed;right:22px;bottom:64px;z-index:60;max-width:390px;
|
|
1157
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);color:var(--fg);
|
|
1158
|
+
border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);border-radius:14px;
|
|
1159
|
+
padding:16px 20px;box-shadow:0 18px 50px rgba(0,0,0,.45);font-size:14px;line-height:1.8;
|
|
1160
|
+
}
|
|
1161
|
+
#help-panel[hidden]{display:none}
|
|
1162
|
+
#help-panel kbd{
|
|
1163
|
+
background:color-mix(in srgb, var(--accent) 22%, transparent);border-radius:5px;
|
|
1164
|
+
padding:1px 6px;font-family:inherit;font-size:12px;
|
|
1165
|
+
}
|
|
1166
|
+
#toast{
|
|
1167
|
+
position:fixed;left:50%;bottom:84px;transform:translateX(-50%);z-index:70;
|
|
1168
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);color:var(--fg);
|
|
1169
|
+
border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);border-radius:99px;
|
|
1170
|
+
padding:9px 18px;font-size:13px;
|
|
1171
|
+
}
|
|
1172
|
+
#toast[hidden]{display:none}
|
|
1173
|
+
#blank{position:fixed;inset:0;z-index:80;display:none;pointer-events:none}
|
|
1174
|
+
body.blank-black #blank{display:block;background:#000}
|
|
1175
|
+
body.blank-white #blank{display:block;background:#fff}
|
|
1176
|
+
body.blank-black #hud,body.blank-white #hud{visibility:hidden;pointer-events:none}
|
|
1177
|
+
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
1178
|
+
body.overview #stage{
|
|
1179
|
+
display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px;
|
|
1180
|
+
grid-auto-rows:var(--deck-thumb-height,180px);
|
|
1181
|
+
padding:18px 18px 92px;position:fixed;inset:0;overflow:auto;align-content:start;
|
|
1182
|
+
}
|
|
1183
|
+
body.overview .slide-frame{
|
|
1184
|
+
position:relative;inset:auto;width:100%;aspect-ratio:16/9;overflow:hidden;
|
|
1185
|
+
border:1px solid color-mix(in srgb, var(--muted) 35%, transparent);border-radius:12px;
|
|
1186
|
+
cursor:pointer;background:var(--bg);outline:none;
|
|
1187
|
+
}
|
|
1188
|
+
body.overview .slide-frame:hover{border-color:var(--accent)}
|
|
1189
|
+
body.overview .slide-frame.is-current{outline:2px solid var(--accent);outline-offset:2px}
|
|
1190
|
+
body.overview .slide-frame:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
|
|
1191
|
+
body.overview .slide-frame::after{
|
|
1192
|
+
content:attr(data-frame);position:absolute;right:8px;bottom:6px;z-index:2;
|
|
1193
|
+
font-size:12px;color:var(--muted);background:color-mix(in srgb, var(--bg) 84%, transparent);
|
|
1194
|
+
border:1px solid color-mix(in srgb, var(--muted) 40%, transparent);border-radius:6px;
|
|
1195
|
+
padding:1px 7px;font-variant-numeric:tabular-nums;
|
|
1196
|
+
}
|
|
1197
|
+
body.overview .slide{
|
|
1198
|
+
display:flex !important;position:absolute;inset:0;width:1280px;height:720px;
|
|
1199
|
+
transform:scale(var(--deck-thumb-scale,.25));transform-origin:top left;pointer-events:none;
|
|
1200
|
+
animation:none !important;
|
|
1201
|
+
}
|
|
1202
|
+
body.overview .bullets li{opacity:1 !important;animation:none !important}
|
|
1203
|
+
body.overview .media-canvas{width:1280px;height:720px}
|
|
1204
|
+
body.overview #notes-panel,body.presenting #notes-panel{display:none !important}
|
|
1205
|
+
body.presenting #notes-toggle{opacity:.55}
|
|
1206
|
+
body.presenting #hud{opacity:0;pointer-events:none;transition:opacity .25s}
|
|
1207
|
+
body.presenting #hud:hover{opacity:1;pointer-events:auto}
|
|
1208
|
+
@media (max-width:900px){
|
|
1209
|
+
.slide{padding:clamp(22px,5vw,44px)}
|
|
1210
|
+
h1{font-size:clamp(34px,8.5vw,64px)}
|
|
1211
|
+
h2{font-size:clamp(26px,6.5vw,48px)}
|
|
1212
|
+
.statement-title{font-size:clamp(26px,7vw,54px)}
|
|
1213
|
+
.subtitle{font-size:clamp(16px,3.4vw,24px)}
|
|
1214
|
+
.bullets li{font-size:clamp(16px,3.4vw,24px)}
|
|
1215
|
+
.kicker{font-size:13px}
|
|
1216
|
+
.deck-table{font-size:14px}
|
|
1217
|
+
.deck-table th,.deck-table td{padding:.55em .6em}
|
|
1218
1218
|
#hud{left:10px;right:10px;bottom:10px;gap:6px;font-size:12px}
|
|
1219
|
-
#hud button{padding:10px 16px;min-height:44px}
|
|
1220
|
-
#notes-panel{bottom:68px;max-height:45vh}
|
|
1221
|
-
#help-panel{right:10px;bottom:68px;left:10px;max-width:none}
|
|
1222
|
-
}
|
|
1223
|
-
@media (max-width:640px){
|
|
1224
|
-
#theme-label{display:none}
|
|
1225
|
-
}
|
|
1226
|
-
@media (prefers-reduced-motion: reduce){
|
|
1227
|
-
*,*::before,*::after{animation:none !important;transition:none !important}
|
|
1228
|
-
}
|
|
1229
|
-
@media print{
|
|
1230
|
-
html,body{height:auto;overflow:visible;background:#fff}
|
|
1231
|
-
.slide-frame{position:relative;display:block !important}
|
|
1232
|
-
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
1233
|
-
${motion ? 'body.motion .bullets li{opacity:1 !important;animation:none !important}' : ''}
|
|
1234
|
-
#progress,#hud,#notes-panel,#help-panel,#toast{display:none !important}
|
|
1235
|
-
}
|
|
1219
|
+
#hud button{padding:10px 16px;min-height:44px}
|
|
1220
|
+
#notes-panel{bottom:68px;max-height:45vh}
|
|
1221
|
+
#help-panel{right:10px;bottom:68px;left:10px;max-width:none}
|
|
1222
|
+
}
|
|
1223
|
+
@media (max-width:640px){
|
|
1224
|
+
#theme-label{display:none}
|
|
1225
|
+
}
|
|
1226
|
+
@media (prefers-reduced-motion: reduce){
|
|
1227
|
+
*,*::before,*::after{animation:none !important;transition:none !important}
|
|
1228
|
+
}
|
|
1229
|
+
@media print{
|
|
1230
|
+
html,body{height:auto;overflow:visible;background:#fff}
|
|
1231
|
+
.slide-frame{position:relative;display:block !important}
|
|
1232
|
+
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
1233
|
+
${motion ? 'body.motion .bullets li{opacity:1 !important;animation:none !important}' : ''}
|
|
1234
|
+
#progress,#hud,#notes-panel,#help-panel,#toast{display:none !important}
|
|
1235
|
+
}
|
|
1236
1236
|
${delivery.css}
|
|
1237
1237
|
</style>
|
|
1238
1238
|
</head>
|
|
1239
1239
|
<body class="${motion ? 'motion' : 'no-motion'}">
|
|
1240
1240
|
<div id="progress" aria-hidden="true"></div>
|
|
1241
1241
|
${delivery.body}
|
|
1242
|
-
<div id="stage">
|
|
1243
|
-
${slides}
|
|
1244
|
-
</div>
|
|
1245
|
-
<div id="hud" aria-live="polite">
|
|
1242
|
+
<div id="stage">
|
|
1243
|
+
${slides}
|
|
1244
|
+
</div>
|
|
1245
|
+
<div id="hud" aria-live="polite">
|
|
1246
1246
|
<span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
|
|
1247
1247
|
${delivery.navigation}
|
|
1248
|
-
<span id="theme-label">${ui.theme} · ${escapeHtml(themeLabel)}</span>
|
|
1249
|
-
<button id="notes-toggle" type="button" title="S · ${escapeHtml(ui.notesToggle)}" aria-controls="notes-panel" aria-expanded="false">${escapeHtml(ui.notesToggle)}</button>
|
|
1250
|
-
<button id="presenter-toggle" type="button" title="V · ${escapeHtml(ui.presenterToggle)}">${escapeHtml(ui.presenterToggle)}</button>
|
|
1251
|
-
<button id="overview-toggle" type="button" title="G · ${escapeHtml(ui.overviewToggle)}" aria-pressed="false">${escapeHtml(ui.overviewToggle)}</button>
|
|
1252
|
-
<button id="help-toggle" type="button" title="?" aria-label="${escapeHtml(ui.helpTitle)}">?</button>
|
|
1253
|
-
<button id="fullscreen" type="button" title="F · Fullscreen" aria-label="Fullscreen">⛶</button>
|
|
1254
|
-
</div>
|
|
1255
|
-
<div id="notes-panel" role="region" aria-label="${escapeHtml(ui.notesLabel)}">
|
|
1256
|
-
<div class="notes-head">
|
|
1257
|
-
<span class="notes-label">${escapeHtml(ui.notesLabel)}</span>
|
|
1258
|
-
<button id="notes-close" type="button" aria-label="${escapeHtml(ui.notesCloseLabel)}">×</button>
|
|
1259
|
-
</div>
|
|
1260
|
-
<div id="notes-text"></div>
|
|
1261
|
-
</div>
|
|
1262
|
-
<div id="help-panel" hidden>
|
|
1263
|
-
<div class="notes-label">${escapeHtml(ui.helpTitle)}</div>
|
|
1264
|
-
<p><kbd>←</kbd> <kbd>→</kbd> / <kbd>Space</kbd> 翻页 · <kbd>Home</kbd> / <kbd>End</kbd> 首末页 · <kbd>S</kbd> ${escapeHtml(ui.notesToggle)} · <kbd>V</kbd> ${escapeHtml(ui.presenterToggle)} · <kbd>G</kbd> ${escapeHtml(ui.overviewToggle)} · <kbd>F</kbd> 全屏 · <kbd>P</kbd> 打印 · <kbd>B</kbd> 黑屏 · <kbd>W</kbd> 白屏 · <kbd>Esc</kbd> ${escapeHtml(ui.closeLabel)} · <kbd>?</kbd> ${escapeHtml(ui.helpTitle)}</p>
|
|
1265
|
-
<p>${escapeHtml(ui.presenterHint)}</p>
|
|
1266
|
-
<p>${escapeHtml(ui.overviewHint)}</p>
|
|
1267
|
-
</div>
|
|
1268
|
-
<div id="toast" role="status" hidden></div>
|
|
1269
|
-
<div id="blank" aria-hidden="true"></div>
|
|
1270
|
-
<script>
|
|
1271
|
-
(() => {
|
|
1272
|
-
const frames = Array.from(document.querySelectorAll('.slide-frame'));
|
|
1273
|
-
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
1274
|
-
const counter = document.getElementById('counter');
|
|
1275
|
-
const progress = document.getElementById('progress');
|
|
1276
|
-
const fullscreenBtn = document.getElementById('fullscreen');
|
|
1277
|
-
const notesPanel = document.getElementById('notes-panel');
|
|
1278
|
-
const notesText = document.getElementById('notes-text');
|
|
1279
|
-
const notesBtn = document.getElementById('notes-toggle');
|
|
1280
|
-
const notesClose = document.getElementById('notes-close');
|
|
1281
|
-
const presenterBtn = document.getElementById('presenter-toggle');
|
|
1282
|
-
const overviewBtn = document.getElementById('overview-toggle');
|
|
1283
|
-
const helpBtn = document.getElementById('help-toggle');
|
|
1284
|
-
const helpPanel = document.getElementById('help-panel');
|
|
1285
|
-
const toastEl = document.getElementById('toast');
|
|
1248
|
+
<span id="theme-label">${ui.theme} · ${escapeHtml(themeLabel)}</span>
|
|
1249
|
+
<button id="notes-toggle" type="button" title="S · ${escapeHtml(ui.notesToggle)}" aria-controls="notes-panel" aria-expanded="false">${escapeHtml(ui.notesToggle)}</button>
|
|
1250
|
+
<button id="presenter-toggle" type="button" title="V · ${escapeHtml(ui.presenterToggle)}">${escapeHtml(ui.presenterToggle)}</button>
|
|
1251
|
+
<button id="overview-toggle" type="button" title="G · ${escapeHtml(ui.overviewToggle)}" aria-pressed="false">${escapeHtml(ui.overviewToggle)}</button>
|
|
1252
|
+
<button id="help-toggle" type="button" title="?" aria-label="${escapeHtml(ui.helpTitle)}">?</button>
|
|
1253
|
+
<button id="fullscreen" type="button" title="F · Fullscreen" aria-label="Fullscreen">⛶</button>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div id="notes-panel" role="region" aria-label="${escapeHtml(ui.notesLabel)}">
|
|
1256
|
+
<div class="notes-head">
|
|
1257
|
+
<span class="notes-label">${escapeHtml(ui.notesLabel)}</span>
|
|
1258
|
+
<button id="notes-close" type="button" aria-label="${escapeHtml(ui.notesCloseLabel)}">×</button>
|
|
1259
|
+
</div>
|
|
1260
|
+
<div id="notes-text"></div>
|
|
1261
|
+
</div>
|
|
1262
|
+
<div id="help-panel" hidden>
|
|
1263
|
+
<div class="notes-label">${escapeHtml(ui.helpTitle)}</div>
|
|
1264
|
+
<p><kbd>←</kbd> <kbd>→</kbd> / <kbd>Space</kbd> 翻页 · <kbd>Home</kbd> / <kbd>End</kbd> 首末页 · <kbd>S</kbd> ${escapeHtml(ui.notesToggle)} · <kbd>V</kbd> ${escapeHtml(ui.presenterToggle)} · <kbd>G</kbd> ${escapeHtml(ui.overviewToggle)} · <kbd>F</kbd> 全屏 · <kbd>P</kbd> 打印 · <kbd>B</kbd> 黑屏 · <kbd>W</kbd> 白屏 · <kbd>Esc</kbd> ${escapeHtml(ui.closeLabel)} · <kbd>?</kbd> ${escapeHtml(ui.helpTitle)}</p>
|
|
1265
|
+
<p>${escapeHtml(ui.presenterHint)}</p>
|
|
1266
|
+
<p>${escapeHtml(ui.overviewHint)}</p>
|
|
1267
|
+
</div>
|
|
1268
|
+
<div id="toast" role="status" hidden></div>
|
|
1269
|
+
<div id="blank" aria-hidden="true"></div>
|
|
1270
|
+
<script>
|
|
1271
|
+
(() => {
|
|
1272
|
+
const frames = Array.from(document.querySelectorAll('.slide-frame'));
|
|
1273
|
+
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
1274
|
+
const counter = document.getElementById('counter');
|
|
1275
|
+
const progress = document.getElementById('progress');
|
|
1276
|
+
const fullscreenBtn = document.getElementById('fullscreen');
|
|
1277
|
+
const notesPanel = document.getElementById('notes-panel');
|
|
1278
|
+
const notesText = document.getElementById('notes-text');
|
|
1279
|
+
const notesBtn = document.getElementById('notes-toggle');
|
|
1280
|
+
const notesClose = document.getElementById('notes-close');
|
|
1281
|
+
const presenterBtn = document.getElementById('presenter-toggle');
|
|
1282
|
+
const overviewBtn = document.getElementById('overview-toggle');
|
|
1283
|
+
const helpBtn = document.getElementById('help-toggle');
|
|
1284
|
+
const helpPanel = document.getElementById('help-panel');
|
|
1285
|
+
const toastEl = document.getElementById('toast');
|
|
1286
1286
|
let index = 0;
|
|
1287
1287
|
let syncDelivery = () => {};
|
|
1288
|
-
let presenterWin = null;
|
|
1289
|
-
let presenterInitSent = false;
|
|
1290
|
-
let toastTimer = 0;
|
|
1291
|
-
const total = slides.length;
|
|
1292
|
-
const ui = ${JSON.stringify(ui).replace(/</g, '\\u003c')};
|
|
1293
|
-
|
|
1294
|
-
function notesOf(i) { return slides[i] && slides[i].dataset ? (slides[i].dataset.notes || '') : ''; }
|
|
1295
|
-
function titleOf(i) {
|
|
1296
|
-
const el = slides[i] ? slides[i].querySelector('h1,h2,.statement-title,.quote-text') : null;
|
|
1297
|
-
return el ? el.textContent.trim() : '';
|
|
1298
|
-
}
|
|
1299
|
-
function state() {
|
|
1300
|
-
const next = (index + 1) % total;
|
|
1301
|
-
return { index: index, total: total, title: titleOf(index), notes: notesOf(index), nextTitle: titleOf(next), nextNotes: notesOf(next) };
|
|
1302
|
-
}
|
|
1303
|
-
window.__dshPpt = { go: function (delta) { go(index + delta); }, jump: function (i) { go(i); }, state: state };
|
|
1304
|
-
|
|
1305
|
-
function toast(message) {
|
|
1306
|
-
toastEl.textContent = message;
|
|
1307
|
-
toastEl.hidden = false;
|
|
1308
|
-
clearTimeout(toastTimer);
|
|
1309
|
-
toastTimer = setTimeout(function () { toastEl.hidden = true; }, 2600);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
function syncNotes() {
|
|
1313
|
-
const notes = notesOf(index);
|
|
1314
|
-
notesText.textContent = notes || ui.noNotes;
|
|
1315
|
-
notesText.classList.toggle('is-empty', notes === '');
|
|
1316
|
-
notesBtn.setAttribute('aria-expanded', document.body.classList.contains('notes-open') ? 'true' : 'false');
|
|
1317
|
-
updatePresenter();
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function sendInit() {
|
|
1321
|
-
if (!presenterOpen()) return;
|
|
1322
|
-
try {
|
|
1323
|
-
presenterWin.postMessage({
|
|
1324
|
-
__dshPpt: 'init',
|
|
1325
|
-
title: (document.title.split(' · ')[1] || document.title),
|
|
1326
|
-
css: document.querySelector('style') ? document.querySelector('style').textContent : '',
|
|
1327
|
-
total: total,
|
|
1328
|
-
slides: slides.map(function (slide) { return slide.outerHTML; }),
|
|
1329
|
-
}, '*');
|
|
1330
|
-
} catch (_) { /* popup closing */ }
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function sendState() {
|
|
1334
|
-
if (!presenterOpen()) return;
|
|
1335
|
-
const next = (index + 1) % total;
|
|
1336
|
-
try {
|
|
1337
|
-
presenterWin.postMessage({
|
|
1338
|
-
__dshPpt: 'state',
|
|
1339
|
-
index: index,
|
|
1340
|
-
total: total,
|
|
1341
|
-
title: titleOf(index),
|
|
1342
|
-
nextTitle: titleOf(next),
|
|
1343
|
-
notes: notesOf(index),
|
|
1344
|
-
nextNotes: notesOf(next),
|
|
1345
|
-
blank: document.body.classList.contains('blank-black') ? 'black' : (document.body.classList.contains('blank-white') ? 'white' : null),
|
|
1346
|
-
}, '*');
|
|
1347
|
-
} catch (_) { /* popup closing */ }
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
function updatePresenter() { sendState(); }
|
|
1351
|
-
|
|
1352
|
-
function go(next) {
|
|
1353
|
-
if (document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) setBlank(null);
|
|
1354
|
-
const wrapped = next < 0 || next >= total;
|
|
1355
|
-
index = (next + total) % total;
|
|
1356
|
-
slides.forEach(function (slide, i) {
|
|
1357
|
-
slide.classList.toggle('is-active', i === index);
|
|
1358
|
-
if (frames[i]) frames[i].classList.toggle('is-current', i === index);
|
|
1359
|
-
});
|
|
1288
|
+
let presenterWin = null;
|
|
1289
|
+
let presenterInitSent = false;
|
|
1290
|
+
let toastTimer = 0;
|
|
1291
|
+
const total = slides.length;
|
|
1292
|
+
const ui = ${JSON.stringify(ui).replace(/</g, '\\u003c')};
|
|
1293
|
+
|
|
1294
|
+
function notesOf(i) { return slides[i] && slides[i].dataset ? (slides[i].dataset.notes || '') : ''; }
|
|
1295
|
+
function titleOf(i) {
|
|
1296
|
+
const el = slides[i] ? slides[i].querySelector('h1,h2,.statement-title,.quote-text') : null;
|
|
1297
|
+
return el ? el.textContent.trim() : '';
|
|
1298
|
+
}
|
|
1299
|
+
function state() {
|
|
1300
|
+
const next = (index + 1) % total;
|
|
1301
|
+
return { index: index, total: total, title: titleOf(index), notes: notesOf(index), nextTitle: titleOf(next), nextNotes: notesOf(next) };
|
|
1302
|
+
}
|
|
1303
|
+
window.__dshPpt = { go: function (delta) { go(index + delta); }, jump: function (i) { go(i); }, state: state };
|
|
1304
|
+
|
|
1305
|
+
function toast(message) {
|
|
1306
|
+
toastEl.textContent = message;
|
|
1307
|
+
toastEl.hidden = false;
|
|
1308
|
+
clearTimeout(toastTimer);
|
|
1309
|
+
toastTimer = setTimeout(function () { toastEl.hidden = true; }, 2600);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function syncNotes() {
|
|
1313
|
+
const notes = notesOf(index);
|
|
1314
|
+
notesText.textContent = notes || ui.noNotes;
|
|
1315
|
+
notesText.classList.toggle('is-empty', notes === '');
|
|
1316
|
+
notesBtn.setAttribute('aria-expanded', document.body.classList.contains('notes-open') ? 'true' : 'false');
|
|
1317
|
+
updatePresenter();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function sendInit() {
|
|
1321
|
+
if (!presenterOpen()) return;
|
|
1322
|
+
try {
|
|
1323
|
+
presenterWin.postMessage({
|
|
1324
|
+
__dshPpt: 'init',
|
|
1325
|
+
title: (document.title.split(' · ')[1] || document.title),
|
|
1326
|
+
css: document.querySelector('style') ? document.querySelector('style').textContent : '',
|
|
1327
|
+
total: total,
|
|
1328
|
+
slides: slides.map(function (slide) { return slide.outerHTML; }),
|
|
1329
|
+
}, '*');
|
|
1330
|
+
} catch (_) { /* popup closing */ }
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function sendState() {
|
|
1334
|
+
if (!presenterOpen()) return;
|
|
1335
|
+
const next = (index + 1) % total;
|
|
1336
|
+
try {
|
|
1337
|
+
presenterWin.postMessage({
|
|
1338
|
+
__dshPpt: 'state',
|
|
1339
|
+
index: index,
|
|
1340
|
+
total: total,
|
|
1341
|
+
title: titleOf(index),
|
|
1342
|
+
nextTitle: titleOf(next),
|
|
1343
|
+
notes: notesOf(index),
|
|
1344
|
+
nextNotes: notesOf(next),
|
|
1345
|
+
blank: document.body.classList.contains('blank-black') ? 'black' : (document.body.classList.contains('blank-white') ? 'white' : null),
|
|
1346
|
+
}, '*');
|
|
1347
|
+
} catch (_) { /* popup closing */ }
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function updatePresenter() { sendState(); }
|
|
1351
|
+
|
|
1352
|
+
function go(next) {
|
|
1353
|
+
if (document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) setBlank(null);
|
|
1354
|
+
const wrapped = next < 0 || next >= total;
|
|
1355
|
+
index = (next + total) % total;
|
|
1356
|
+
slides.forEach(function (slide, i) {
|
|
1357
|
+
slide.classList.toggle('is-active', i === index);
|
|
1358
|
+
if (frames[i]) frames[i].classList.toggle('is-current', i === index);
|
|
1359
|
+
});
|
|
1360
1360
|
counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
|
|
1361
1361
|
syncDelivery(index);
|
|
1362
|
-
progress.style.width = ((index + 1) / total * 100) + '%';
|
|
1363
|
-
document.title = (index + 1) + ' / ' + total + ' · ' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
1364
|
-
try { history.replaceState?.(null, '', '#slide-' + (index + 1)); } catch (_) { /* file:// 下个别浏览器可能拒绝 */ }
|
|
1365
|
-
if (wrapped) toast(ui.wrapHint + ' ' + (index + 1) + ' / ' + total);
|
|
1366
|
-
syncNotes();
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
function notesOpen() { return document.body.classList.contains('notes-open'); }
|
|
1370
|
-
function presenterOpen() { try { return !!(presenterWin && !presenterWin.closed); } catch (_) { return false; } }
|
|
1371
|
-
|
|
1372
|
-
function setNotes(on) {
|
|
1373
|
-
document.body.classList.toggle('notes-open', on);
|
|
1374
|
-
if (on) setOverview(false);
|
|
1375
|
-
syncNotes();
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function updateThumbScale() {
|
|
1379
|
-
if (!document.body.classList.contains('overview')) return;
|
|
1380
|
-
requestAnimationFrame(function () {
|
|
1381
|
-
const width = frames[0] ? frames[0].getBoundingClientRect().width : 0;
|
|
1382
|
-
if (width > 0) {
|
|
1383
|
-
document.documentElement.style.setProperty('--deck-thumb-scale', String(width / 1280));
|
|
1384
|
-
document.documentElement.style.setProperty('--deck-thumb-height', String(width * 9 / 16) + 'px');
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
function setOverview(on) {
|
|
1390
|
-
document.body.classList.toggle('overview', on);
|
|
1391
|
-
overviewBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1392
|
-
frames.forEach(function (frame) { frame.tabIndex = on ? 0 : -1; });
|
|
1393
|
-
if (on) {
|
|
1394
|
-
document.body.classList.remove('notes-open');
|
|
1395
|
-
helpPanel.hidden = true;
|
|
1396
|
-
syncNotes();
|
|
1397
|
-
updateThumbScale();
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
function presenterHtml() {
|
|
1402
|
-
const labelCurrent = ${JSON.stringify(ui.currentLabel)};
|
|
1403
|
-
const labelNext = ${JSON.stringify(ui.nextLabel)};
|
|
1404
|
-
const labelNotes = ${JSON.stringify(ui.notesLabel)};
|
|
1405
|
-
const noNotes = ${JSON.stringify(ui.noNotes)};
|
|
1406
|
-
const hintText = ${JSON.stringify('← → 翻页 · P 暂停 · T 重置 · +/- 字号 · B/W 黑/白屏 · Esc 结束')};
|
|
1407
|
-
const deckTitle = ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
1408
|
-
const langAttr = ${JSON.stringify(lang.attr)};
|
|
1409
|
-
const cs = getComputedStyle(document.documentElement);
|
|
1410
|
-
const val = function (name, fallback) { const v = cs.getPropertyValue(name).trim(); return v || fallback; };
|
|
1411
|
-
const css = [
|
|
1412
|
-
':root{--bg:' + val('--bg', '#070B14') + ';--panel:' + val('--panel', '#0D1424') + ';--fg:' + val('--fg', '#E8F1FF') + ';--muted:' + val('--muted', '#7E8BA8') + ';--accent:' + val('--accent', '#7C3AED') + ';--accent2:' + val('--accent2', '#06B6D4') + ';--font-heading:' + val('--font-heading', 'sans-serif') + ';--font-body:' + val('--font-body', 'sans-serif') + '}',
|
|
1413
|
-
'*{box-sizing:border-box}',
|
|
1414
|
-
'body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-body);height:100vh;display:flex;flex-direction:column;overflow:hidden}',
|
|
1415
|
-
'header{display:flex;align-items:center;gap:14px;padding:9px 16px;border-bottom:1px solid color-mix(in srgb, var(--muted) 40%, transparent);font-size:13px;color:var(--muted)}',
|
|
1416
|
-
'header #ptitle{color:var(--fg);font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
1417
|
-
'header #pclock{font-variant-numeric:tabular-nums}',
|
|
1418
|
-
'main{flex:1;display:grid;grid-template-columns:1.12fr .88fr;gap:12px;padding:12px 16px;min-height:0}',
|
|
1419
|
-
'.col{display:flex;flex-direction:column;gap:10px;min-height:0}',
|
|
1420
|
-
'.card{background:var(--panel);border:1px solid color-mix(in srgb, var(--muted) 30%, transparent);border-radius:12px;padding:10px 12px;min-height:0;display:flex;flex-direction:column;gap:6px}',
|
|
1421
|
-
'.card-notes{flex:1}',
|
|
1422
|
-
'.lbl{color:var(--accent);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}',
|
|
1423
|
-
'.pv{position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:8px;background:var(--bg);border:1px solid color-mix(in srgb, var(--muted) 25%, transparent);--pv-scale:.3}',
|
|
1424
|
-
'.pv-slide{position:absolute;left:0;top:0;transform:translate(var(--pv-x,0px),var(--pv-y,0px)) scale(var(--pv-scale,.25));transform-origin:top left}',
|
|
1425
|
-
'.pv-slide .slide{display:flex !important;position:absolute;left:0;top:0;width:100vw;height:100vh;animation:none !important}',
|
|
1426
|
-
'.pv-slide .bullets li{opacity:1 !important;animation:none !important}',
|
|
1427
|
-
'#thumbs{display:flex;gap:8px;overflow-x:auto;padding-bottom:2px}',
|
|
1428
|
-
'#thumbs .thumb{position:relative;flex:0 0 128px;aspect-ratio:16/9;border-radius:8px;overflow:hidden;border:1px solid color-mix(in srgb, var(--muted) 35%, transparent);cursor:pointer;background:var(--bg);--pv-scale:.09}',
|
|
1429
|
-
'#thumbs .thumb.is-current{outline:2px solid var(--accent);outline-offset:1px}',
|
|
1430
|
-
'#pnotes{font-size:var(--notes-size,19px);line-height:1.6;white-space:pre-wrap;overflow:auto;flex:1}',
|
|
1431
|
-
'footer{display:flex;align-items:center;gap:8px;padding:9px 16px;border-top:1px solid color-mix(in srgb, var(--muted) 40%, transparent);flex-wrap:wrap}',
|
|
1432
|
-
'button{background:color-mix(in srgb, var(--panel) 90%, transparent);color:var(--fg);border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;padding:6px 12px;min-height:34px;font:inherit;font-size:13px;cursor:pointer}',
|
|
1433
|
-
'button:hover{border-color:var(--accent);color:var(--accent)}',
|
|
1434
|
-
'button[aria-pressed="true"]{background:var(--accent);color:var(--bg);border-color:var(--accent)}',
|
|
1435
|
-
'#ptimer{font-variant-numeric:tabular-nums;font-size:20px;font-weight:700;color:var(--accent);margin-left:6px}',
|
|
1436
|
-
'.bar{flex:1;min-width:100px;height:8px;background:color-mix(in srgb, var(--muted) 28%, transparent);border-radius:99px;overflow:hidden}',
|
|
1437
|
-
'#pprog{height:100%;width:0;background:var(--accent);transition:width .2s}',
|
|
1438
|
-
'.hint{color:var(--muted);font-size:11px;width:100%}',
|
|
1439
|
-
].join('');
|
|
1440
|
-
const js = [
|
|
1441
|
-
'var total=0,index=0,elapsed=0,started=Date.now(),paused=false,notesSize=19,slides=[],cssText="",curBlank=null,initialized=false,readyTries=0;',
|
|
1442
|
-
'function q(id){return document.getElementById(id)}',
|
|
1443
|
-
'function post(m){try{if(window.opener)window.opener.postMessage(m,"*")}catch(e){}}',
|
|
1444
|
-
'function fmt(ms){var s=Math.max(0,Math.floor(ms/1000)),m=Math.floor(s/60);s=s%60;var h=Math.floor(m/60);m=m%60;return (h>0?(h<10?"0":"")+h+":":"")+(m<10?"0":"")+m+":"+(s<10?"0":"")+s}',
|
|
1445
|
-
'function clock(){var d=new Date();return ("0"+d.getHours()).slice(-2)+":"+("0"+d.getMinutes()).slice(-2)+":"+("0"+d.getSeconds()).slice(-2)}',
|
|
1446
|
-
'function tick(){if(!paused)elapsed=Date.now()-started;q("ptimer").textContent=fmt(elapsed);q("pclock").textContent=clock();if(window.requestAnimationFrame)requestAnimationFrame(tick);else setTimeout(tick,250)}',
|
|
1447
|
-
'function fitBox(box){var rw=box.clientWidth||320,rh=box.clientHeight||180;var s=Math.min(rw/window.innerWidth,rh/window.innerHeight);var ox=(rw-window.innerWidth*s)/2,oy=(rh-window.innerHeight*s)/2;box.style.setProperty("--pv-scale",String(s));box.style.setProperty("--pv-x",ox+"px");box.style.setProperty("--pv-y",oy+"px")}',
|
|
1448
|
-
'function fit(){fitBox(q("pv-cur"));fitBox(q("pv-next"));var th=q("thumbs").children;for(var i=0;i<th.length;i++){fitBox(th[i])}}',
|
|
1449
|
-
'function setSlide(box,html){box.innerHTML="";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;box.appendChild(f)}',
|
|
1450
|
-
'function buildThumbs(){var box=q("thumbs");box.innerHTML="";slides.forEach(function(html,i){var t=document.createElement("div");t.className="thumb";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;t.appendChild(f);t.addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",index:i})});box.appendChild(t)});fit()}',
|
|
1451
|
-
'function renderState(st){index=st.index;total=st.total||total;q("pcounter").textContent=(index+1)+" / "+total;setSlide(q("pv-cur"),slides[index]||"");setSlide(q("pv-next"),slides[(index+1)%total]||"");q("pnotes").textContent=st.notes||' + JSON.stringify(ui.noNotes) + ';q("pprog").style.width=((index+1)/total*100)+"%";curBlank=st.blank||null;q("pv-black").setAttribute("aria-pressed",curBlank==="black"?"true":"false");q("pv-white").setAttribute("aria-pressed",curBlank==="white"?"true":"false");q("pv-black").textContent=curBlank==="black"?"取消黑屏":"黑屏";q("pv-white").textContent=curBlank==="white"?"取消白屏":"白屏";var th=q("thumbs").children;for(var i=0;i<th.length;i++){th[i].classList.toggle("is-current",i===index)}fit()}',
|
|
1452
|
-
'function togglePause(){paused=!paused;if(!paused)started=Date.now()-elapsed;var b=q("pause");b.setAttribute("aria-pressed",paused?"true":"false");b.textContent=paused?"继续":"暂停"}',
|
|
1453
|
-
'function resetTimer(){started=Date.now();elapsed=0}',
|
|
1454
|
-
'function applyNotesSize(){document.documentElement.style.setProperty("--notes-size",notesSize+"px")}',
|
|
1455
|
-
'window.addEventListener("message",function(e){var d=e.data||{};if(d.__dshPpt==="init"&&!initialized){initialized=true;slides=d.slides||[];total=d.total||slides.length;cssText=d.css||"";var st=document.createElement("style");st.textContent=cssText;document.head.appendChild(st);q("ptitle").textContent=d.title||"";buildThumbs()}else if(d.__dshPpt==="state"){renderState(d)}else if(d.__dshPpt==="close"){window.close()}});',
|
|
1456
|
-
'window.addEventListener("resize",fit);',
|
|
1457
|
-
'window.addEventListener("beforeunload",function(){post({__dshPpt:"cmd",kind:"closed"})});',
|
|
1458
|
-
'document.addEventListener("keydown",function(e){var k=e.key;if(k==="ArrowRight"||k===" "){e.preventDefault();post({__dshPpt:"cmd",kind:"go",delta:1})}else if(k==="ArrowLeft"){e.preventDefault();post({__dshPpt:"cmd",kind:"go",delta:-1})}else if(k==="Escape"){post({__dshPpt:"cmd",kind:"end"});window.close()}else if(k.toLowerCase()==="p"){togglePause()}else if(k.toLowerCase()==="t"){resetTimer()}else if(k==="+"||k==="="){notesSize=Math.min(34,notesSize+2);applyNotesSize()}else if(k==="-"){notesSize=Math.max(12,notesSize-2);applyNotesSize()}else if(k.toLowerCase()==="b"){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="black"?null:"black"})}else if(k.toLowerCase()==="w"){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="white"?null:"white"})}});',
|
|
1459
|
-
'q("prev").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",delta:-1})});',
|
|
1460
|
-
'q("next").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",delta:1})});',
|
|
1461
|
-
'q("pause").addEventListener("click",togglePause);',
|
|
1462
|
-
'q("reset").addEventListener("click",resetTimer);',
|
|
1463
|
-
'q("font-plus").addEventListener("click",function(){notesSize=Math.min(34,notesSize+2);applyNotesSize()});',
|
|
1464
|
-
'q("font-minus").addEventListener("click",function(){notesSize=Math.max(12,notesSize-2);applyNotesSize()});',
|
|
1465
|
-
'q("pv-black").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="black"?null:"black"})});',
|
|
1466
|
-
'q("pv-white").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="white"?null:"white"})});',
|
|
1467
|
-
'q("end").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"end"});window.close()});',
|
|
1468
|
-
'applyNotesSize();tick();function ready(){if(initialized)return;post({__dshPpt:"ready"});readyTries++;if(readyTries<5)setTimeout(ready,700)}setTimeout(ready,250);',
|
|
1469
|
-
].join('');
|
|
1470
|
-
return '<!DOCTYPE html><html lang="' + langAttr + '"><head><meta charset="UTF-8"><title>' + deckTitle + ' · 演讲者视图</title><style>' + css + '</style></head><body>'
|
|
1471
|
-
+ '<header><div id="ptitle"></div><div id="pcounter"></div><div id="pclock" title="本地时间"></div></header>'
|
|
1472
|
-
+ '<main><div class="col">'
|
|
1473
|
-
+ '<div class="card"><div class="lbl">' + labelCurrent + '</div><div class="pv" id="pv-cur"></div></div>'
|
|
1474
|
-
+ '<div class="card"><div class="lbl">' + labelNext + '</div><div class="pv" id="pv-next"></div></div>'
|
|
1475
|
-
+ '<div class="card"><div class="lbl">SLIDES</div><div id="thumbs"></div></div>'
|
|
1476
|
-
+ '</div><div class="col"><div class="card card-notes"><div class="lbl">' + labelNotes + '</div><div id="pnotes"></div></div></div></main>'
|
|
1477
|
-
+ '<footer><button id="prev" title="上一页 (←)">◀</button><button id="next" title="下一页 (→)">▶</button><button id="pause" title="暂停 (P)" aria-pressed="false">暂停</button><button id="reset" title="重置计时 (T)">重置计时</button><button id="font-minus" title="备注字号减小 (-)">A−</button><button id="font-plus" title="备注字号增大 (+)">A+</button><button id="pv-black" title="黑屏 (B)">黑屏</button><button id="pv-white" title="白屏 (W)">白屏</button><button id="end" title="结束放映 (Esc)">结束放映</button><div id="ptimer">00:00</div><div class="bar"><div id="pprog"></div></div><div class="hint">' + hintText + '</div></footer>'
|
|
1478
|
-
+ '<scr' + 'ipt>' + js + '</scr' + 'ipt></body></html>';
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
async function toggleFullscreen() {
|
|
1482
|
-
try {
|
|
1483
|
-
if (document.fullscreenElement) { await document.exitFullscreen?.(); return; }
|
|
1484
|
-
if (!document.documentElement.requestFullscreen || document.fullscreenEnabled === false) {
|
|
1485
|
-
toast(ui.fullscreenUnavailable);
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
await document.documentElement.requestFullscreen();
|
|
1489
|
-
if (!document.fullscreenElement) toast(ui.fullscreenUnavailable);
|
|
1490
|
-
} catch (_) { toast(ui.fullscreenUnavailable); }
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
function openPresenter() {
|
|
1494
|
-
if (presenterOpen()) { try { presenterWin.focus(); } catch (_) { /* gone */ } return; }
|
|
1495
|
-
let url = '';
|
|
1496
|
-
try {
|
|
1497
|
-
url = URL.createObjectURL(new Blob([presenterHtml()], { type: 'text/html' }));
|
|
1498
|
-
} catch (_) { toast(ui.popupBlocked); return; }
|
|
1499
|
-
presenterInitSent = false;
|
|
1500
|
-
presenterWin = window.open(url, 'dsh-ppt-presenter', 'width=1180,height=760');
|
|
1501
|
-
if (!presenterWin) {
|
|
1502
|
-
try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ }
|
|
1503
|
-
toast(ui.popupBlocked);
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
document.body.classList.add('presenting');
|
|
1507
|
-
if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur();
|
|
1508
|
-
setTimeout(function () { try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ } }, 5000);
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
function closePresenter() {
|
|
1512
|
-
if (presenterOpen()) {
|
|
1513
|
-
try { presenterWin.postMessage({ __dshPpt: 'close' }, '*'); presenterWin.close(); } catch (_) { /* gone */ }
|
|
1514
|
-
}
|
|
1515
|
-
document.body.classList.remove('presenting');
|
|
1516
|
-
presenterInitSent = false;
|
|
1517
|
-
if (document.fullscreenElement) { try { document.exitFullscreen?.(); } catch (_) { /* ignore */ } }
|
|
1518
|
-
setBlank(null);
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function setBlank(color) {
|
|
1522
|
-
document.body.classList.remove('blank-black', 'blank-white');
|
|
1523
|
-
if (color === 'black') document.body.classList.add('blank-black');
|
|
1524
|
-
else if (color === 'white') document.body.classList.add('blank-white');
|
|
1525
|
-
sendState();
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
setInterval(function () {
|
|
1529
|
-
if (!presenterOpen() && document.body.classList.contains('presenting')) {
|
|
1530
|
-
document.body.classList.remove('presenting');
|
|
1531
|
-
if (document.fullscreenElement) { try { document.exitFullscreen?.(); } catch (_) { /* ignore */ } }
|
|
1532
|
-
setBlank(null);
|
|
1533
|
-
}
|
|
1534
|
-
}, 800);
|
|
1535
|
-
|
|
1536
|
-
window.addEventListener('message', function (event) {
|
|
1537
|
-
if (event.source !== presenterWin) return;
|
|
1538
|
-
const data = event.data || {};
|
|
1539
|
-
if (data.__dshPpt === 'cmd' && (data.kind === 'end' || data.kind === 'closed')) { closePresenter(); return; }
|
|
1540
|
-
if (!presenterOpen()) return;
|
|
1541
|
-
if (data.__dshPpt === 'ready') {
|
|
1542
|
-
if (!presenterInitSent) { presenterInitSent = true; sendInit(); }
|
|
1543
|
-
sendState();
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
if (data.__dshPpt !== 'cmd') return;
|
|
1547
|
-
if (data.kind === 'go') {
|
|
1548
|
-
setBlank(null);
|
|
1549
|
-
if (typeof data.index === 'number') go(data.index);
|
|
1550
|
-
else go(index + (data.delta || 0));
|
|
1551
|
-
sendState();
|
|
1552
|
-
} else if (data.kind === 'blank') {
|
|
1553
|
-
setBlank(data.color || null);
|
|
1554
|
-
} else if (data.kind === 'end') {
|
|
1555
|
-
closePresenter();
|
|
1556
|
-
} else if (data.kind === 'closed') {
|
|
1557
|
-
document.body.classList.remove('presenting');
|
|
1558
|
-
setBlank(null);
|
|
1559
|
-
}
|
|
1560
|
-
});
|
|
1561
|
-
|
|
1562
|
-
document.addEventListener('keydown', (event) => {
|
|
1563
|
-
const key = event.key;
|
|
1564
|
-
const lower = key.toLowerCase();
|
|
1362
|
+
progress.style.width = ((index + 1) / total * 100) + '%';
|
|
1363
|
+
document.title = (index + 1) + ' / ' + total + ' · ' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
1364
|
+
try { history.replaceState?.(null, '', '#slide-' + (index + 1)); } catch (_) { /* file:// 下个别浏览器可能拒绝 */ }
|
|
1365
|
+
if (wrapped) toast(ui.wrapHint + ' ' + (index + 1) + ' / ' + total);
|
|
1366
|
+
syncNotes();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function notesOpen() { return document.body.classList.contains('notes-open'); }
|
|
1370
|
+
function presenterOpen() { try { return !!(presenterWin && !presenterWin.closed); } catch (_) { return false; } }
|
|
1371
|
+
|
|
1372
|
+
function setNotes(on) {
|
|
1373
|
+
document.body.classList.toggle('notes-open', on);
|
|
1374
|
+
if (on) setOverview(false);
|
|
1375
|
+
syncNotes();
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function updateThumbScale() {
|
|
1379
|
+
if (!document.body.classList.contains('overview')) return;
|
|
1380
|
+
requestAnimationFrame(function () {
|
|
1381
|
+
const width = frames[0] ? frames[0].getBoundingClientRect().width : 0;
|
|
1382
|
+
if (width > 0) {
|
|
1383
|
+
document.documentElement.style.setProperty('--deck-thumb-scale', String(width / 1280));
|
|
1384
|
+
document.documentElement.style.setProperty('--deck-thumb-height', String(width * 9 / 16) + 'px');
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function setOverview(on) {
|
|
1390
|
+
document.body.classList.toggle('overview', on);
|
|
1391
|
+
overviewBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1392
|
+
frames.forEach(function (frame) { frame.tabIndex = on ? 0 : -1; });
|
|
1393
|
+
if (on) {
|
|
1394
|
+
document.body.classList.remove('notes-open');
|
|
1395
|
+
helpPanel.hidden = true;
|
|
1396
|
+
syncNotes();
|
|
1397
|
+
updateThumbScale();
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function presenterHtml() {
|
|
1402
|
+
const labelCurrent = ${JSON.stringify(ui.currentLabel)};
|
|
1403
|
+
const labelNext = ${JSON.stringify(ui.nextLabel)};
|
|
1404
|
+
const labelNotes = ${JSON.stringify(ui.notesLabel)};
|
|
1405
|
+
const noNotes = ${JSON.stringify(ui.noNotes)};
|
|
1406
|
+
const hintText = ${JSON.stringify('← → 翻页 · P 暂停 · T 重置 · +/- 字号 · B/W 黑/白屏 · Esc 结束')};
|
|
1407
|
+
const deckTitle = ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
1408
|
+
const langAttr = ${JSON.stringify(lang.attr)};
|
|
1409
|
+
const cs = getComputedStyle(document.documentElement);
|
|
1410
|
+
const val = function (name, fallback) { const v = cs.getPropertyValue(name).trim(); return v || fallback; };
|
|
1411
|
+
const css = [
|
|
1412
|
+
':root{--bg:' + val('--bg', '#070B14') + ';--panel:' + val('--panel', '#0D1424') + ';--fg:' + val('--fg', '#E8F1FF') + ';--muted:' + val('--muted', '#7E8BA8') + ';--accent:' + val('--accent', '#7C3AED') + ';--accent2:' + val('--accent2', '#06B6D4') + ';--font-heading:' + val('--font-heading', 'sans-serif') + ';--font-body:' + val('--font-body', 'sans-serif') + '}',
|
|
1413
|
+
'*{box-sizing:border-box}',
|
|
1414
|
+
'body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-body);height:100vh;display:flex;flex-direction:column;overflow:hidden}',
|
|
1415
|
+
'header{display:flex;align-items:center;gap:14px;padding:9px 16px;border-bottom:1px solid color-mix(in srgb, var(--muted) 40%, transparent);font-size:13px;color:var(--muted)}',
|
|
1416
|
+
'header #ptitle{color:var(--fg);font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
1417
|
+
'header #pclock{font-variant-numeric:tabular-nums}',
|
|
1418
|
+
'main{flex:1;display:grid;grid-template-columns:1.12fr .88fr;gap:12px;padding:12px 16px;min-height:0}',
|
|
1419
|
+
'.col{display:flex;flex-direction:column;gap:10px;min-height:0}',
|
|
1420
|
+
'.card{background:var(--panel);border:1px solid color-mix(in srgb, var(--muted) 30%, transparent);border-radius:12px;padding:10px 12px;min-height:0;display:flex;flex-direction:column;gap:6px}',
|
|
1421
|
+
'.card-notes{flex:1}',
|
|
1422
|
+
'.lbl{color:var(--accent);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}',
|
|
1423
|
+
'.pv{position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:8px;background:var(--bg);border:1px solid color-mix(in srgb, var(--muted) 25%, transparent);--pv-scale:.3}',
|
|
1424
|
+
'.pv-slide{position:absolute;left:0;top:0;transform:translate(var(--pv-x,0px),var(--pv-y,0px)) scale(var(--pv-scale,.25));transform-origin:top left}',
|
|
1425
|
+
'.pv-slide .slide{display:flex !important;position:absolute;left:0;top:0;width:100vw;height:100vh;animation:none !important}',
|
|
1426
|
+
'.pv-slide .bullets li{opacity:1 !important;animation:none !important}',
|
|
1427
|
+
'#thumbs{display:flex;gap:8px;overflow-x:auto;padding-bottom:2px}',
|
|
1428
|
+
'#thumbs .thumb{position:relative;flex:0 0 128px;aspect-ratio:16/9;border-radius:8px;overflow:hidden;border:1px solid color-mix(in srgb, var(--muted) 35%, transparent);cursor:pointer;background:var(--bg);--pv-scale:.09}',
|
|
1429
|
+
'#thumbs .thumb.is-current{outline:2px solid var(--accent);outline-offset:1px}',
|
|
1430
|
+
'#pnotes{font-size:var(--notes-size,19px);line-height:1.6;white-space:pre-wrap;overflow:auto;flex:1}',
|
|
1431
|
+
'footer{display:flex;align-items:center;gap:8px;padding:9px 16px;border-top:1px solid color-mix(in srgb, var(--muted) 40%, transparent);flex-wrap:wrap}',
|
|
1432
|
+
'button{background:color-mix(in srgb, var(--panel) 90%, transparent);color:var(--fg);border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;padding:6px 12px;min-height:34px;font:inherit;font-size:13px;cursor:pointer}',
|
|
1433
|
+
'button:hover{border-color:var(--accent);color:var(--accent)}',
|
|
1434
|
+
'button[aria-pressed="true"]{background:var(--accent);color:var(--bg);border-color:var(--accent)}',
|
|
1435
|
+
'#ptimer{font-variant-numeric:tabular-nums;font-size:20px;font-weight:700;color:var(--accent);margin-left:6px}',
|
|
1436
|
+
'.bar{flex:1;min-width:100px;height:8px;background:color-mix(in srgb, var(--muted) 28%, transparent);border-radius:99px;overflow:hidden}',
|
|
1437
|
+
'#pprog{height:100%;width:0;background:var(--accent);transition:width .2s}',
|
|
1438
|
+
'.hint{color:var(--muted);font-size:11px;width:100%}',
|
|
1439
|
+
].join('');
|
|
1440
|
+
const js = [
|
|
1441
|
+
'var total=0,index=0,elapsed=0,started=Date.now(),paused=false,notesSize=19,slides=[],cssText="",curBlank=null,initialized=false,readyTries=0;',
|
|
1442
|
+
'function q(id){return document.getElementById(id)}',
|
|
1443
|
+
'function post(m){try{if(window.opener)window.opener.postMessage(m,"*")}catch(e){}}',
|
|
1444
|
+
'function fmt(ms){var s=Math.max(0,Math.floor(ms/1000)),m=Math.floor(s/60);s=s%60;var h=Math.floor(m/60);m=m%60;return (h>0?(h<10?"0":"")+h+":":"")+(m<10?"0":"")+m+":"+(s<10?"0":"")+s}',
|
|
1445
|
+
'function clock(){var d=new Date();return ("0"+d.getHours()).slice(-2)+":"+("0"+d.getMinutes()).slice(-2)+":"+("0"+d.getSeconds()).slice(-2)}',
|
|
1446
|
+
'function tick(){if(!paused)elapsed=Date.now()-started;q("ptimer").textContent=fmt(elapsed);q("pclock").textContent=clock();if(window.requestAnimationFrame)requestAnimationFrame(tick);else setTimeout(tick,250)}',
|
|
1447
|
+
'function fitBox(box){var rw=box.clientWidth||320,rh=box.clientHeight||180;var s=Math.min(rw/window.innerWidth,rh/window.innerHeight);var ox=(rw-window.innerWidth*s)/2,oy=(rh-window.innerHeight*s)/2;box.style.setProperty("--pv-scale",String(s));box.style.setProperty("--pv-x",ox+"px");box.style.setProperty("--pv-y",oy+"px")}',
|
|
1448
|
+
'function fit(){fitBox(q("pv-cur"));fitBox(q("pv-next"));var th=q("thumbs").children;for(var i=0;i<th.length;i++){fitBox(th[i])}}',
|
|
1449
|
+
'function setSlide(box,html){box.innerHTML="";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;box.appendChild(f)}',
|
|
1450
|
+
'function buildThumbs(){var box=q("thumbs");box.innerHTML="";slides.forEach(function(html,i){var t=document.createElement("div");t.className="thumb";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;t.appendChild(f);t.addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",index:i})});box.appendChild(t)});fit()}',
|
|
1451
|
+
'function renderState(st){index=st.index;total=st.total||total;q("pcounter").textContent=(index+1)+" / "+total;setSlide(q("pv-cur"),slides[index]||"");setSlide(q("pv-next"),slides[(index+1)%total]||"");q("pnotes").textContent=st.notes||' + JSON.stringify(ui.noNotes) + ';q("pprog").style.width=((index+1)/total*100)+"%";curBlank=st.blank||null;q("pv-black").setAttribute("aria-pressed",curBlank==="black"?"true":"false");q("pv-white").setAttribute("aria-pressed",curBlank==="white"?"true":"false");q("pv-black").textContent=curBlank==="black"?"取消黑屏":"黑屏";q("pv-white").textContent=curBlank==="white"?"取消白屏":"白屏";var th=q("thumbs").children;for(var i=0;i<th.length;i++){th[i].classList.toggle("is-current",i===index)}fit()}',
|
|
1452
|
+
'function togglePause(){paused=!paused;if(!paused)started=Date.now()-elapsed;var b=q("pause");b.setAttribute("aria-pressed",paused?"true":"false");b.textContent=paused?"继续":"暂停"}',
|
|
1453
|
+
'function resetTimer(){started=Date.now();elapsed=0}',
|
|
1454
|
+
'function applyNotesSize(){document.documentElement.style.setProperty("--notes-size",notesSize+"px")}',
|
|
1455
|
+
'window.addEventListener("message",function(e){var d=e.data||{};if(d.__dshPpt==="init"&&!initialized){initialized=true;slides=d.slides||[];total=d.total||slides.length;cssText=d.css||"";var st=document.createElement("style");st.textContent=cssText;document.head.appendChild(st);q("ptitle").textContent=d.title||"";buildThumbs()}else if(d.__dshPpt==="state"){renderState(d)}else if(d.__dshPpt==="close"){window.close()}});',
|
|
1456
|
+
'window.addEventListener("resize",fit);',
|
|
1457
|
+
'window.addEventListener("beforeunload",function(){post({__dshPpt:"cmd",kind:"closed"})});',
|
|
1458
|
+
'document.addEventListener("keydown",function(e){var k=e.key;if(k==="ArrowRight"||k===" "){e.preventDefault();post({__dshPpt:"cmd",kind:"go",delta:1})}else if(k==="ArrowLeft"){e.preventDefault();post({__dshPpt:"cmd",kind:"go",delta:-1})}else if(k==="Escape"){post({__dshPpt:"cmd",kind:"end"});window.close()}else if(k.toLowerCase()==="p"){togglePause()}else if(k.toLowerCase()==="t"){resetTimer()}else if(k==="+"||k==="="){notesSize=Math.min(34,notesSize+2);applyNotesSize()}else if(k==="-"){notesSize=Math.max(12,notesSize-2);applyNotesSize()}else if(k.toLowerCase()==="b"){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="black"?null:"black"})}else if(k.toLowerCase()==="w"){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="white"?null:"white"})}});',
|
|
1459
|
+
'q("prev").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",delta:-1})});',
|
|
1460
|
+
'q("next").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"go",delta:1})});',
|
|
1461
|
+
'q("pause").addEventListener("click",togglePause);',
|
|
1462
|
+
'q("reset").addEventListener("click",resetTimer);',
|
|
1463
|
+
'q("font-plus").addEventListener("click",function(){notesSize=Math.min(34,notesSize+2);applyNotesSize()});',
|
|
1464
|
+
'q("font-minus").addEventListener("click",function(){notesSize=Math.max(12,notesSize-2);applyNotesSize()});',
|
|
1465
|
+
'q("pv-black").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="black"?null:"black"})});',
|
|
1466
|
+
'q("pv-white").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"blank",color:curBlank==="white"?null:"white"})});',
|
|
1467
|
+
'q("end").addEventListener("click",function(){post({__dshPpt:"cmd",kind:"end"});window.close()});',
|
|
1468
|
+
'applyNotesSize();tick();function ready(){if(initialized)return;post({__dshPpt:"ready"});readyTries++;if(readyTries<5)setTimeout(ready,700)}setTimeout(ready,250);',
|
|
1469
|
+
].join('');
|
|
1470
|
+
return '<!DOCTYPE html><html lang="' + langAttr + '"><head><meta charset="UTF-8"><title>' + deckTitle + ' · 演讲者视图</title><style>' + css + '</style></head><body>'
|
|
1471
|
+
+ '<header><div id="ptitle"></div><div id="pcounter"></div><div id="pclock" title="本地时间"></div></header>'
|
|
1472
|
+
+ '<main><div class="col">'
|
|
1473
|
+
+ '<div class="card"><div class="lbl">' + labelCurrent + '</div><div class="pv" id="pv-cur"></div></div>'
|
|
1474
|
+
+ '<div class="card"><div class="lbl">' + labelNext + '</div><div class="pv" id="pv-next"></div></div>'
|
|
1475
|
+
+ '<div class="card"><div class="lbl">SLIDES</div><div id="thumbs"></div></div>'
|
|
1476
|
+
+ '</div><div class="col"><div class="card card-notes"><div class="lbl">' + labelNotes + '</div><div id="pnotes"></div></div></div></main>'
|
|
1477
|
+
+ '<footer><button id="prev" title="上一页 (←)">◀</button><button id="next" title="下一页 (→)">▶</button><button id="pause" title="暂停 (P)" aria-pressed="false">暂停</button><button id="reset" title="重置计时 (T)">重置计时</button><button id="font-minus" title="备注字号减小 (-)">A−</button><button id="font-plus" title="备注字号增大 (+)">A+</button><button id="pv-black" title="黑屏 (B)">黑屏</button><button id="pv-white" title="白屏 (W)">白屏</button><button id="end" title="结束放映 (Esc)">结束放映</button><div id="ptimer">00:00</div><div class="bar"><div id="pprog"></div></div><div class="hint">' + hintText + '</div></footer>'
|
|
1478
|
+
+ '<scr' + 'ipt>' + js + '</scr' + 'ipt></body></html>';
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async function toggleFullscreen() {
|
|
1482
|
+
try {
|
|
1483
|
+
if (document.fullscreenElement) { await document.exitFullscreen?.(); return; }
|
|
1484
|
+
if (!document.documentElement.requestFullscreen || document.fullscreenEnabled === false) {
|
|
1485
|
+
toast(ui.fullscreenUnavailable);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
await document.documentElement.requestFullscreen();
|
|
1489
|
+
if (!document.fullscreenElement) toast(ui.fullscreenUnavailable);
|
|
1490
|
+
} catch (_) { toast(ui.fullscreenUnavailable); }
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function openPresenter() {
|
|
1494
|
+
if (presenterOpen()) { try { presenterWin.focus(); } catch (_) { /* gone */ } return; }
|
|
1495
|
+
let url = '';
|
|
1496
|
+
try {
|
|
1497
|
+
url = URL.createObjectURL(new Blob([presenterHtml()], { type: 'text/html' }));
|
|
1498
|
+
} catch (_) { toast(ui.popupBlocked); return; }
|
|
1499
|
+
presenterInitSent = false;
|
|
1500
|
+
presenterWin = window.open(url, 'dsh-ppt-presenter', 'width=1180,height=760');
|
|
1501
|
+
if (!presenterWin) {
|
|
1502
|
+
try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ }
|
|
1503
|
+
toast(ui.popupBlocked);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
document.body.classList.add('presenting');
|
|
1507
|
+
if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur();
|
|
1508
|
+
setTimeout(function () { try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ } }, 5000);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function closePresenter() {
|
|
1512
|
+
if (presenterOpen()) {
|
|
1513
|
+
try { presenterWin.postMessage({ __dshPpt: 'close' }, '*'); presenterWin.close(); } catch (_) { /* gone */ }
|
|
1514
|
+
}
|
|
1515
|
+
document.body.classList.remove('presenting');
|
|
1516
|
+
presenterInitSent = false;
|
|
1517
|
+
if (document.fullscreenElement) { try { document.exitFullscreen?.(); } catch (_) { /* ignore */ } }
|
|
1518
|
+
setBlank(null);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function setBlank(color) {
|
|
1522
|
+
document.body.classList.remove('blank-black', 'blank-white');
|
|
1523
|
+
if (color === 'black') document.body.classList.add('blank-black');
|
|
1524
|
+
else if (color === 'white') document.body.classList.add('blank-white');
|
|
1525
|
+
sendState();
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
setInterval(function () {
|
|
1529
|
+
if (!presenterOpen() && document.body.classList.contains('presenting')) {
|
|
1530
|
+
document.body.classList.remove('presenting');
|
|
1531
|
+
if (document.fullscreenElement) { try { document.exitFullscreen?.(); } catch (_) { /* ignore */ } }
|
|
1532
|
+
setBlank(null);
|
|
1533
|
+
}
|
|
1534
|
+
}, 800);
|
|
1535
|
+
|
|
1536
|
+
window.addEventListener('message', function (event) {
|
|
1537
|
+
if (event.source !== presenterWin) return;
|
|
1538
|
+
const data = event.data || {};
|
|
1539
|
+
if (data.__dshPpt === 'cmd' && (data.kind === 'end' || data.kind === 'closed')) { closePresenter(); return; }
|
|
1540
|
+
if (!presenterOpen()) return;
|
|
1541
|
+
if (data.__dshPpt === 'ready') {
|
|
1542
|
+
if (!presenterInitSent) { presenterInitSent = true; sendInit(); }
|
|
1543
|
+
sendState();
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (data.__dshPpt !== 'cmd') return;
|
|
1547
|
+
if (data.kind === 'go') {
|
|
1548
|
+
setBlank(null);
|
|
1549
|
+
if (typeof data.index === 'number') go(data.index);
|
|
1550
|
+
else go(index + (data.delta || 0));
|
|
1551
|
+
sendState();
|
|
1552
|
+
} else if (data.kind === 'blank') {
|
|
1553
|
+
setBlank(data.color || null);
|
|
1554
|
+
} else if (data.kind === 'end') {
|
|
1555
|
+
closePresenter();
|
|
1556
|
+
} else if (data.kind === 'closed') {
|
|
1557
|
+
document.body.classList.remove('presenting');
|
|
1558
|
+
setBlank(null);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
document.addEventListener('keydown', (event) => {
|
|
1563
|
+
const key = event.key;
|
|
1564
|
+
const lower = key.toLowerCase();
|
|
1565
1565
|
const targetEl = event.target;
|
|
1566
1566
|
if (document.getElementById('review-panel').open || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
1567
1567
|
if (key !== 'Escape' && targetEl && (['INPUT', 'SELECT', 'TEXTAREA'].includes(targetEl.tagName) || targetEl.isContentEditable === true)) return;
|
|
1568
|
-
const interactive = targetEl && (targetEl.tagName === 'BUTTON' || targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT' || targetEl.tagName === 'TEXTAREA' || targetEl.isContentEditable === true);
|
|
1569
|
-
if (key === ' ' && interactive) return;
|
|
1570
|
-
if (key === 'Escape') {
|
|
1571
|
-
if (!helpPanel.hidden) { helpPanel.hidden = true; return; }
|
|
1572
|
-
if (document.body.classList.contains('overview')) { setOverview(false); return; }
|
|
1573
|
-
if (notesOpen()) { setNotes(false); return; }
|
|
1574
|
-
if (document.body.classList.contains('presenting') || document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) { closePresenter(); return; }
|
|
1575
|
-
if (document.fullscreenElement) { document.exitFullscreen?.(); return; }
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
const inOverview = document.body.classList.contains('overview');
|
|
1579
|
-
if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || (key === 'ArrowDown' && !inOverview)) {
|
|
1580
|
-
event.preventDefault(); go(index + 1);
|
|
1581
|
-
} else if (key === 'ArrowLeft' || key === 'PageUp' || (key === 'ArrowUp' && !inOverview)) {
|
|
1582
|
-
event.preventDefault(); go(index - 1);
|
|
1583
|
-
} else if (/^[1-9]$/.test(key) && !inOverview) {
|
|
1584
|
-
const jumped = Number(key);
|
|
1585
|
-
if (jumped <= total) { event.preventDefault(); go(jumped - 1); }
|
|
1586
|
-
} else if (key === 'Home') {
|
|
1587
|
-
event.preventDefault(); go(0);
|
|
1588
|
-
} else if (key === 'End') {
|
|
1589
|
-
event.preventDefault(); go(total - 1);
|
|
1590
|
-
} else if (key === 'Enter' && inOverview) {
|
|
1591
|
-
event.preventDefault(); setOverview(false);
|
|
1592
|
-
} else if (lower === 'f') {
|
|
1593
|
-
void toggleFullscreen();
|
|
1594
|
-
} else if (lower === 'g') {
|
|
1595
|
-
setOverview(!inOverview);
|
|
1596
|
-
} else if (lower === 's') {
|
|
1597
|
-
if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); }
|
|
1598
|
-
else { document.body.classList.toggle('notes-open'); syncNotes(); }
|
|
1599
|
-
} else if (lower === 'v') {
|
|
1600
|
-
if (presenterOpen()) closePresenter();
|
|
1601
|
-
else openPresenter();
|
|
1602
|
-
} else if (lower === 'b') {
|
|
1603
|
-
setBlank(document.body.classList.contains('blank-black') ? null : 'black');
|
|
1604
|
-
} else if (lower === 'w') {
|
|
1605
|
-
setBlank(document.body.classList.contains('blank-white') ? null : 'white');
|
|
1606
|
-
} else if (lower === 'p') {
|
|
1607
|
-
window.print();
|
|
1608
|
-
} else if (key === '?') {
|
|
1609
|
-
helpPanel.hidden = !helpPanel.hidden;
|
|
1610
|
-
}
|
|
1611
|
-
});
|
|
1612
|
-
|
|
1613
|
-
let wheelLock = 0;
|
|
1568
|
+
const interactive = targetEl && (targetEl.tagName === 'BUTTON' || targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT' || targetEl.tagName === 'TEXTAREA' || targetEl.isContentEditable === true);
|
|
1569
|
+
if (key === ' ' && interactive) return;
|
|
1570
|
+
if (key === 'Escape') {
|
|
1571
|
+
if (!helpPanel.hidden) { helpPanel.hidden = true; return; }
|
|
1572
|
+
if (document.body.classList.contains('overview')) { setOverview(false); return; }
|
|
1573
|
+
if (notesOpen()) { setNotes(false); return; }
|
|
1574
|
+
if (document.body.classList.contains('presenting') || document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) { closePresenter(); return; }
|
|
1575
|
+
if (document.fullscreenElement) { document.exitFullscreen?.(); return; }
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const inOverview = document.body.classList.contains('overview');
|
|
1579
|
+
if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || (key === 'ArrowDown' && !inOverview)) {
|
|
1580
|
+
event.preventDefault(); go(index + 1);
|
|
1581
|
+
} else if (key === 'ArrowLeft' || key === 'PageUp' || (key === 'ArrowUp' && !inOverview)) {
|
|
1582
|
+
event.preventDefault(); go(index - 1);
|
|
1583
|
+
} else if (/^[1-9]$/.test(key) && !inOverview) {
|
|
1584
|
+
const jumped = Number(key);
|
|
1585
|
+
if (jumped <= total) { event.preventDefault(); go(jumped - 1); }
|
|
1586
|
+
} else if (key === 'Home') {
|
|
1587
|
+
event.preventDefault(); go(0);
|
|
1588
|
+
} else if (key === 'End') {
|
|
1589
|
+
event.preventDefault(); go(total - 1);
|
|
1590
|
+
} else if (key === 'Enter' && inOverview) {
|
|
1591
|
+
event.preventDefault(); setOverview(false);
|
|
1592
|
+
} else if (lower === 'f') {
|
|
1593
|
+
void toggleFullscreen();
|
|
1594
|
+
} else if (lower === 'g') {
|
|
1595
|
+
setOverview(!inOverview);
|
|
1596
|
+
} else if (lower === 's') {
|
|
1597
|
+
if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); }
|
|
1598
|
+
else { document.body.classList.toggle('notes-open'); syncNotes(); }
|
|
1599
|
+
} else if (lower === 'v') {
|
|
1600
|
+
if (presenterOpen()) closePresenter();
|
|
1601
|
+
else openPresenter();
|
|
1602
|
+
} else if (lower === 'b') {
|
|
1603
|
+
setBlank(document.body.classList.contains('blank-black') ? null : 'black');
|
|
1604
|
+
} else if (lower === 'w') {
|
|
1605
|
+
setBlank(document.body.classList.contains('blank-white') ? null : 'white');
|
|
1606
|
+
} else if (lower === 'p') {
|
|
1607
|
+
window.print();
|
|
1608
|
+
} else if (key === '?') {
|
|
1609
|
+
helpPanel.hidden = !helpPanel.hidden;
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
let wheelLock = 0;
|
|
1614
1614
|
document.addEventListener('wheel', (event) => {
|
|
1615
1615
|
if (document.getElementById('review-panel').open || event.target.closest?.('#hud,#delivery-bar,#notes-panel,#help-panel')) return;
|
|
1616
|
-
const now = Date.now();
|
|
1617
|
-
if (now - wheelLock < 550 || document.body.classList.contains('overview') || notesOpen()) return;
|
|
1618
|
-
wheelLock = now;
|
|
1619
|
-
if (Math.abs(event.deltaY) > 12) go(index + (event.deltaY > 0 ? 1 : -1));
|
|
1620
|
-
}, { passive: true });
|
|
1621
|
-
|
|
1616
|
+
const now = Date.now();
|
|
1617
|
+
if (now - wheelLock < 550 || document.body.classList.contains('overview') || notesOpen()) return;
|
|
1618
|
+
wheelLock = now;
|
|
1619
|
+
if (Math.abs(event.deltaY) > 12) go(index + (event.deltaY > 0 ? 1 : -1));
|
|
1620
|
+
}, { passive: true });
|
|
1621
|
+
|
|
1622
1622
|
let touchStartY = 0;
|
|
1623
1623
|
let touchOnStage = false;
|
|
1624
1624
|
document.addEventListener('touchstart', (event) => { touchOnStage = !document.getElementById('review-panel').open && !!event.target.closest?.('#stage'); touchStartY = event.touches[0].clientY; }, { passive: true });
|
|
1625
|
-
document.addEventListener('touchend', (event) => {
|
|
1626
|
-
const delta = event.changedTouches[0].clientY - touchStartY;
|
|
1625
|
+
document.addEventListener('touchend', (event) => {
|
|
1626
|
+
const delta = event.changedTouches[0].clientY - touchStartY;
|
|
1627
1627
|
if (touchOnStage && Math.abs(delta) > 48 && !document.body.classList.contains('overview')) go(index + (delta < 0 ? 1 : -1));
|
|
1628
|
-
}, { passive: true });
|
|
1629
|
-
|
|
1630
|
-
fullscreenBtn.addEventListener('click', () => { void toggleFullscreen(); });
|
|
1631
|
-
notesBtn.addEventListener('click', () => {
|
|
1632
|
-
if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); return; }
|
|
1633
|
-
setNotes(!notesOpen());
|
|
1634
|
-
});
|
|
1635
|
-
notesClose.addEventListener('click', () => setNotes(false));
|
|
1636
|
-
presenterBtn.addEventListener('click', () => { if (presenterOpen()) closePresenter(); else openPresenter(); });
|
|
1637
|
-
overviewBtn.addEventListener('click', () => setOverview(!document.body.classList.contains('overview')));
|
|
1638
|
-
helpBtn.addEventListener('click', () => { helpPanel.hidden = !helpPanel.hidden; });
|
|
1639
|
-
frames.forEach((frame, i) => {
|
|
1640
|
-
frame.addEventListener('click', () => {
|
|
1641
|
-
if (document.body.classList.contains('overview')) { go(i); setOverview(false); }
|
|
1642
|
-
});
|
|
1643
|
-
frame.addEventListener('keydown', (event) => {
|
|
1644
|
-
if (event.key === 'Enter' && document.body.classList.contains('overview')) { go(i); setOverview(false); }
|
|
1645
|
-
});
|
|
1646
|
-
});
|
|
1647
|
-
window.addEventListener('resize', updateThumbScale);
|
|
1648
|
-
window.addEventListener('beforeunload', closePresenter);
|
|
1649
|
-
|
|
1628
|
+
}, { passive: true });
|
|
1629
|
+
|
|
1630
|
+
fullscreenBtn.addEventListener('click', () => { void toggleFullscreen(); });
|
|
1631
|
+
notesBtn.addEventListener('click', () => {
|
|
1632
|
+
if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); return; }
|
|
1633
|
+
setNotes(!notesOpen());
|
|
1634
|
+
});
|
|
1635
|
+
notesClose.addEventListener('click', () => setNotes(false));
|
|
1636
|
+
presenterBtn.addEventListener('click', () => { if (presenterOpen()) closePresenter(); else openPresenter(); });
|
|
1637
|
+
overviewBtn.addEventListener('click', () => setOverview(!document.body.classList.contains('overview')));
|
|
1638
|
+
helpBtn.addEventListener('click', () => { helpPanel.hidden = !helpPanel.hidden; });
|
|
1639
|
+
frames.forEach((frame, i) => {
|
|
1640
|
+
frame.addEventListener('click', () => {
|
|
1641
|
+
if (document.body.classList.contains('overview')) { go(i); setOverview(false); }
|
|
1642
|
+
});
|
|
1643
|
+
frame.addEventListener('keydown', (event) => {
|
|
1644
|
+
if (event.key === 'Enter' && document.body.classList.contains('overview')) { go(i); setOverview(false); }
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
window.addEventListener('resize', updateThumbScale);
|
|
1648
|
+
window.addEventListener('beforeunload', closePresenter);
|
|
1649
|
+
|
|
1650
1650
|
${delivery.script}
|
|
1651
1651
|
const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
|
|
1652
|
-
go(Number.isInteger(start) ? start - 1 : 0);
|
|
1653
|
-
})();
|
|
1654
|
-
</script>
|
|
1655
|
-
</body>
|
|
1656
|
-
</html>`
|
|
1657
|
-
}
|
|
1658
|
-
function renderHtmlSlide(slide, index, ui, langId, manifest = {}, theme) {
|
|
1659
|
-
const kicker = slide.kicker || (slide.layout === 'cover' ? ui.coverKicker : '')
|
|
1660
|
-
const title = slide.title || ''
|
|
1661
|
-
const subtitle = slide.subtitle || ''
|
|
1662
|
-
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
1663
|
-
const number = String(index + 1).padStart(2, '0')
|
|
1664
|
-
let inner = ''
|
|
1665
|
-
switch (slide.layout) {
|
|
1666
|
-
case 'image':
|
|
1667
|
-
case 'image-left':
|
|
1668
|
-
case 'image-right':
|
|
1669
|
-
case 'chart':
|
|
1670
|
-
inner = renderMediaHtml(slide, manifest, theme)
|
|
1671
|
-
break
|
|
1672
|
-
case 'cover':
|
|
1673
|
-
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1674
|
-
'<h1>' + escapeHtml(title) + '</h1>' +
|
|
1675
|
-
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
1676
|
-
'<div class="meta">' + escapeHtml(ui.generatedBy) + '</div>'
|
|
1677
|
-
break
|
|
1678
|
-
case 'section':
|
|
1679
|
-
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1680
|
-
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
1681
|
-
'<div class="accent-line"></div>' +
|
|
1682
|
-
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
1683
|
-
break
|
|
1684
|
-
case 'statement':
|
|
1685
|
-
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1686
|
-
'<div class="statement-title">' + escapeHtml(title || slide.text || '') + '</div>' +
|
|
1687
|
-
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
1688
|
-
break
|
|
1689
|
-
case 'quote':
|
|
1690
|
-
inner = '<div class="kicker">' + escapeHtml(kicker || ui.quoteKicker) + '</div>' +
|
|
1691
|
-
'<div class="quote-mark" aria-hidden="true">\u201C</div>' +
|
|
1692
|
-
'<blockquote class="quote-text">' + escapeHtml(title || slide.text || '') + '</blockquote>' +
|
|
1693
|
-
(subtitle !== '' ? '<div class="quote-attr">\u2014\u2014 ' + escapeHtml(subtitle) + '</div>' : '')
|
|
1694
|
-
break
|
|
1695
|
-
case 'table': {
|
|
1696
|
-
const rows = Array.isArray(slide.rows) ? slide.rows : []
|
|
1697
|
-
const head = rows[0] ?? []
|
|
1698
|
-
const body = rows.slice(1)
|
|
1699
|
-
const isNum = (cell) => /^\s*[+-]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?\s*(?:%|%|次|页|个|人|元|天|小时|分钟)?\s*$/.test(String(cell ?? ''))
|
|
1700
|
-
const headingText = title !== '' ? title : (kicker || ui.tableKicker)
|
|
1701
|
-
const showKicker = kicker !== '' && kicker !== headingText
|
|
1702
|
-
inner = (showKicker ? '<div class="kicker">' + escapeHtml(kicker) + '</div>' : '') +
|
|
1703
|
-
'<h2>' + escapeHtml(headingText) + '</h2>' +
|
|
1704
|
-
'<table class="deck-table"><thead><tr>' +
|
|
1705
|
-
head.map((cell) => '<th' + (isNum(cell) ? ' class="num"' : '') + '>' + escapeHtml(cell) + '</th>').join('') +
|
|
1706
|
-
'</tr></thead><tbody>' +
|
|
1707
|
-
body.map((row) => '<tr>' + row.map((cell) => '<td' + (isNum(cell) ? ' class="num"' : '') + '>' + escapeHtml(cell) + '</td>').join('') + '</tr>').join('') +
|
|
1708
|
-
'</tbody></table>'
|
|
1709
|
-
break
|
|
1710
|
-
}
|
|
1711
|
-
case 'closing':
|
|
1712
|
-
inner = '<h1>' + escapeHtml(title || ui.closingTitle) + '</h1>' +
|
|
1713
|
-
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
1714
|
-
'<div class="meta">' + escapeHtml(ui.generatedBy) + '</div>'
|
|
1715
|
-
break
|
|
1716
|
-
case 'bullets':
|
|
1717
|
-
default:
|
|
1718
|
-
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1719
|
-
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
1720
|
-
'<ul>' + bullets.map((bullet, bulletIndex) => '<li style="--i:' + bulletIndex + '">' + escapeHtml(bullet) + '</li>').join('') + '</ul>'
|
|
1721
|
-
break
|
|
1722
|
-
}
|
|
1723
|
-
const notesAttr = typeof slide.notes === 'string' && slide.notes.trim() !== ''
|
|
1724
|
-
? ' data-notes="' + escapeHtml(slide.notes) + '"'
|
|
1725
|
-
: ''
|
|
1726
|
-
return '<section class="slide slide--' + escapeHtml(slide.layout || 'bullets') + '" data-index="' + number + '"' + notesAttr + '>' +
|
|
1727
|
-
'<div class="slide-inner">' + inner + '</div>' +
|
|
1728
|
-
(manifest.brand?.logoAssetId ? '<img class="brand-logo" src="' + escapeHtml(manifest.assets?.[manifest.brand.logoAssetId]?.data ?? '') + '" alt="' + escapeHtml(manifest.brand.name || 'Logo') + '"/>' : '') +
|
|
1729
|
-
(manifest.brand?.footer ? '<div class="brand-footer">' + escapeHtml(manifest.brand.footer) + '</div>' : '') + '</section>'
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
function hexToRgba(hex, alpha) {
|
|
1733
|
-
const value = String(hex).replace('#', '')
|
|
1734
|
-
const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value
|
|
1735
|
-
const num = Number.parseInt(full, 16)
|
|
1736
|
-
const r = (num >> 16) & 255
|
|
1737
|
-
const g = (num >> 8) & 255
|
|
1738
|
-
const b = num & 255
|
|
1739
|
-
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
// ---------------------------------------------------------------------------
|
|
1743
|
-
// PPTX(OOXML 手写 + node:zlib zip)
|
|
1744
|
-
// ---------------------------------------------------------------------------
|
|
1745
|
-
|
|
1746
|
-
const EMU_W = 12192000
|
|
1747
|
-
const EMU_H = 6858000
|
|
1748
|
-
|
|
1749
|
-
function hex(value) {
|
|
1750
|
-
return String(value).replace('#', '').toUpperCase()
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
export function pptxFontName(value) {
|
|
1754
|
-
const names = String(value ?? '').split(',').map(name => name.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
|
|
1755
|
-
// A CSS stack may start with an optional web font. Office receives one actual
|
|
1756
|
-
// family, so select a platform fallback instead of copying the first name.
|
|
1757
|
-
if (names.length === 1 && !['serif', 'sans-serif', 'system-ui'].includes(names[0])) return names[0]
|
|
1758
|
-
const available = process.platform === 'win32'
|
|
1759
|
-
? ['Segoe UI', 'Arial', 'Arial Black', 'Impact', 'Georgia', 'Times New Roman', 'Microsoft YaHei', 'SimSun']
|
|
1760
|
-
: process.platform === 'darwin'
|
|
1761
|
-
? ['Helvetica Neue', 'Arial', 'Impact', 'Georgia', 'Times New Roman', 'Avenir Next', 'PingFang SC', 'Songti SC']
|
|
1762
|
-
: ['Liberation Sans', 'Liberation Serif', 'Noto Sans', 'Noto Sans CJK SC', 'Noto Serif CJK SC']
|
|
1763
|
-
return names.find(name => available.includes(name)) || (names.includes('serif')
|
|
1764
|
-
? process.platform === 'linux' ? 'Liberation Serif' : 'Georgia'
|
|
1765
|
-
: process.platform === 'win32' ? 'Segoe UI' : process.platform === 'darwin' ? 'Helvetica Neue' : 'Liberation Sans')
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
export function pptxEastAsianFont(value) {
|
|
1769
|
-
const font = pptxFontName(value)
|
|
1770
|
-
if (/Microsoft YaHei|SimSun|SimHei|PingFang|Songti|Heiti|Noto (Sans|Serif) CJK|[\u3400-\u9fff]/i.test(font)) return font
|
|
1771
|
-
const serif = /Georgia|Times New Roman|Cambria|Liberation Serif/i.test(font)
|
|
1772
|
-
return process.platform === 'win32' ? serif ? 'SimSun' : 'Microsoft YaHei'
|
|
1773
|
-
: process.platform === 'darwin' ? serif ? 'Songti SC' : 'PingFang SC'
|
|
1774
|
-
: serif ? 'Noto Serif CJK SC' : 'Noto Sans CJK SC'
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function paragraphXml(text, options = {}) {
|
|
1778
|
-
const {
|
|
1779
|
-
size = 2000,
|
|
1780
|
-
color = 'FFFFFF',
|
|
1781
|
-
bold = false,
|
|
1782
|
-
lang = 'zh-CN',
|
|
1783
|
-
align = 'l',
|
|
1784
|
-
bullet = false,
|
|
1785
|
-
font = 'Arial',
|
|
1786
|
-
spaceBefore = 0,
|
|
1787
|
-
spaceAfter = 0,
|
|
1788
|
-
} = options
|
|
1789
|
-
const runFont = pptxFontName(font)
|
|
1790
|
-
const spacing = (spaceBefore > 0 ? '<a:spcBef><a:spcPts val="' + spaceBefore + '"/></a:spcBef>' : '') +
|
|
1791
|
-
(spaceAfter > 0 ? '<a:spcAft><a:spcPts val="' + spaceAfter + '"/></a:spcAft>' : '')
|
|
1792
|
-
const pPr = '<a:pPr' + (bullet ? ' marL="285750" indent="-285750"' : align !== 'l' ? ' algn="' + align + '"' : '') + '>' + spacing +
|
|
1793
|
-
(bullet ? '<a:buFont typeface="Arial" panose="020B0604020202020204"/><a:buChar char="•"/>' : '<a:buNone/>') + '</a:pPr>'
|
|
1794
|
-
return '<a:p>' + pPr +
|
|
1795
|
-
'<a:r><a:rPr lang="' + lang + '" sz="' + size + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1796
|
-
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1797
|
-
'<a:latin typeface="' + escapeXml(runFont) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(font)) + '"/></a:rPr>' +
|
|
1798
|
-
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p>'
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
function textShapeXml(id, name, box, paragraphs, options = {}) {
|
|
1802
|
-
const { x = 0, y = 0, w = EMU_W, h = EMU_H } = box
|
|
1803
|
-
return '<p:sp>' +
|
|
1804
|
-
'<p:nvSpPr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
|
|
1805
|
-
'<p:spPr><a:xfrm><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + w + '" cy="' + h + '"/></a:xfrm>' +
|
|
1806
|
-
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln/></p:spPr>' +
|
|
1807
|
-
'<p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:normAutofit/></a:bodyPr><a:lstStyle/>' +
|
|
1808
|
-
paragraphs.join('') + '</p:txBody></p:sp>'
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
function accentBarXml(id, name, box, color) {
|
|
1812
|
-
const { x = 0, y = 0, w = EMU_W, h = EMU_H } = box
|
|
1813
|
-
return '<p:sp>' +
|
|
1814
|
-
'<p:nvSpPr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
|
|
1815
|
-
'<p:spPr><a:xfrm><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + w + '" cy="' + h + '"/></a:xfrm>' +
|
|
1816
|
-
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' +
|
|
1817
|
-
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill><a:ln/></p:spPr>' +
|
|
1818
|
-
'<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>'
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
/** 原生 OOXML 表格(graphicFrame + a:tbl):首行主题色表头,隔行面板色。 */
|
|
1822
|
-
function tableGraphicXml(id, name, box, rows, theme, langId) {
|
|
1823
|
-
const p = theme.palette
|
|
1824
|
-
const lang = langId || 'zh-CN'
|
|
1825
|
-
const font = pptxFontName(theme.fonts.body)
|
|
1826
|
-
const safeRows = rows.length > 0 ? rows : [['']]
|
|
1827
|
-
const cols = Math.max(1, safeRows[0].length)
|
|
1828
|
-
const fontSize = cols <= 6 ? 1700 : 1400
|
|
1829
|
-
const colW = Math.floor(box.w / cols)
|
|
1830
|
-
const grid = Array.from({ length: cols }, () => '<a:gridCol w="' + colW + '"/>').join('')
|
|
1831
|
-
const rowH = 420000
|
|
1832
|
-
const cellXml = (text, fill, color, bold) =>
|
|
1833
|
-
'<a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="l"><a:buNone/></a:pPr>' +
|
|
1834
|
-
'<a:r><a:rPr lang="' + lang + '" sz="' + fontSize + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1835
|
-
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1836
|
-
'<a:latin typeface="' + escapeXml(font) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/></a:rPr>' +
|
|
1837
|
-
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p></a:txBody>' +
|
|
1838
|
-
'<a:tcPr marL="91440" marR="91440" anchor="ctr">' +
|
|
1839
|
-
(fill !== '' ? '<a:solidFill><a:srgbClr val="' + hex(fill) + '"/></a:solidFill>' : '<a:noFill/>') +
|
|
1840
|
-
'</a:tcPr></a:tc>'
|
|
1841
|
-
const trs = safeRows.map((row, rowIndex) => {
|
|
1842
|
-
const padded = Array.from({ length: cols }, (_, i) => row[i] ?? '')
|
|
1843
|
-
const cells = rowIndex === 0
|
|
1844
|
-
? padded.map((c) => cellXml(c, p.accent, p.bg, true)).join('')
|
|
1845
|
-
: padded.map((c) => cellXml(c, rowIndex % 2 === 1 ? p.panel : '', p.fg, false)).join('')
|
|
1846
|
-
return '<a:tr h="' + rowH + '">' + cells + '</a:tr>'
|
|
1847
|
-
}).join('')
|
|
1848
|
-
const height = Math.max(1, safeRows.length) * rowH
|
|
1849
|
-
return '<p:graphicFrame>' +
|
|
1850
|
-
'<p:nvGraphicFramePr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/>' +
|
|
1851
|
-
'<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>' +
|
|
1852
|
-
'<p:xfrm><a:off x="' + box.x + '" y="' + box.y + '"/><a:ext cx="' + box.w + '" cy="' + height + '"/></p:xfrm>' +
|
|
1853
|
-
'<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">' +
|
|
1854
|
-
'<a:tbl><a:tblPr firstRow="1" bandRow="1"/>' +
|
|
1855
|
-
'<a:tblGrid>' + grid + '</a:tblGrid>' + trs + '</a:tbl>' +
|
|
1856
|
-
'</a:graphicData></a:graphic></p:graphicFrame>'
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
function slideShapeList(slide, index, total, theme, langId) {
|
|
1860
|
-
const p = theme.palette
|
|
1861
|
-
const font = pptxFontName(theme.fonts.heading)
|
|
1862
|
-
const bodyFont = pptxFontName(theme.fonts.body) // 保留:v0.1 后续用于统一正文字体
|
|
1863
|
-
const lang = langId || 'zh-CN'
|
|
1864
|
-
// PPTX 每个文本 run 的语言跟随 deck.lang(zh-CN / en-US),不再写死中文
|
|
1865
|
-
const para = (content, options = {}) => paragraphXml(content, { lang, ...options })
|
|
1866
|
-
const shapes = []
|
|
1867
|
-
const kicker = slide.kicker || ''
|
|
1868
|
-
const title = slide.title || ''
|
|
1869
|
-
const subtitle = slide.subtitle || ''
|
|
1870
|
-
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
1871
|
-
const idBase = (index + 1) * 10
|
|
1872
|
-
const footer = String(index + 1).padStart(2, '0') + ' / ' + String(total).padStart(2, '0')
|
|
1873
|
-
|
|
1874
|
-
if (MEDIA_LAYOUTS.has(slide.layout)) {
|
|
1875
|
-
const boxes = mediaBoxes(slide.layout)
|
|
1876
|
-
const emu = box => Object.fromEntries(Object.entries(box).map(([key, value]) => [key, Math.round(value * 914400)]))
|
|
1877
|
-
shapes.push(textShapeXml(idBase + 2, 'Title', emu(boxes.title), [para(title, { size: 3400, color: p.fg, bold: true, font })]))
|
|
1878
|
-
if (boxes.body) shapes.push(textShapeXml(idBase + 3, 'Body', emu(boxes.body), [
|
|
1879
|
-
...(subtitle ? [para(subtitle, { size: 2000, color: p.fg, font: bodyFont })] : []),
|
|
1880
|
-
...bullets.map(bullet => para(bullet, { size: 2000, color: p.fg, bullet: true, font: bodyFont, spaceAfter: 600 })),
|
|
1881
|
-
]))
|
|
1882
|
-
const caption = slide.chart?.caption || slide.image?.caption
|
|
1883
|
-
if (caption) shapes.push(textShapeXml(idBase + 4, 'Caption', emu(boxes.caption), [para(caption, { size: 1400, color: p.muted, font: bodyFont })]))
|
|
1884
|
-
} else if (slide.layout === 'cover') {
|
|
1885
|
-
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 2250000, w: 240000, h: 1600000 }, p.accent))
|
|
1886
|
-
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 1550000, y: 2160000, w: 9250000, h: 1800000 }, [
|
|
1887
|
-
para(title, { size: 4400, color: p.fg, bold: true, font }),
|
|
1888
|
-
]))
|
|
1889
|
-
if (subtitle !== '') {
|
|
1890
|
-
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1570000, y: 4150000, w: 9000000, h: 1200000 }, [
|
|
1891
|
-
para(subtitle, { size: 2200, color: p.muted, font: theme.fonts.body }),
|
|
1892
|
-
]))
|
|
1893
|
-
}
|
|
1894
|
-
if (kicker !== '') {
|
|
1895
|
-
shapes.push(textShapeXml(idBase + 4, 'Kicker', { x: 1570000, y: 5800000, w: 7000000, h: 500000 }, [
|
|
1896
|
-
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1897
|
-
]))
|
|
1898
|
-
}
|
|
1899
|
-
} else if (slide.layout === 'section') {
|
|
1900
|
-
if (kicker !== '') {
|
|
1901
|
-
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 1050000, y: 2250000, w: 9000000, h: 500000 }, [
|
|
1902
|
-
para(kicker, { size: 1600, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1903
|
-
]))
|
|
1904
|
-
}
|
|
1905
|
-
shapes.push(accentBarXml(idBase + 2, 'Accent bar', { x: 1050000, y: 2850000, w: 1600000, h: 160000 }, p.accent2))
|
|
1906
|
-
shapes.push(textShapeXml(idBase + 3, 'Title', { x: 1050000, y: 3150000, w: 10200000, h: 1400000 }, [
|
|
1907
|
-
para(title, { size: 4000, color: p.fg, bold: true, font }),
|
|
1908
|
-
]))
|
|
1909
|
-
if (subtitle !== '') {
|
|
1910
|
-
shapes.push(textShapeXml(idBase + 4, 'Subtitle', { x: 1070000, y: 4750000, w: 9000000, h: 900000 }, [
|
|
1911
|
-
para(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
1912
|
-
]))
|
|
1913
|
-
}
|
|
1914
|
-
} else if (slide.layout === 'statement') {
|
|
1915
|
-
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 1900000, w: 200000, h: 2800000 }, p.accent))
|
|
1916
|
-
shapes.push(textShapeXml(idBase + 2, 'Statement', { x: 1500000, y: 1950000, w: 9400000, h: 2700000 }, [
|
|
1917
|
-
para(title || slide.text || '', { size: 3600, color: p.fg, bold: true, font }),
|
|
1918
|
-
]))
|
|
1919
|
-
if (subtitle !== '') {
|
|
1920
|
-
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1520000, y: 4900000, w: 9000000, h: 800000 }, [
|
|
1921
|
-
para(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
1922
|
-
]))
|
|
1923
|
-
}
|
|
1924
|
-
} else if (slide.layout === 'quote') {
|
|
1925
|
-
shapes.push(textShapeXml(idBase + 1, 'Quote mark', { x: 900000, y: 1050000, w: 1700000, h: 1900000 }, [
|
|
1926
|
-
para('\u201C', { size: 9600, color: p.accent, bold: true, font }),
|
|
1927
|
-
]))
|
|
1928
|
-
shapes.push(textShapeXml(idBase + 2, 'Quote', { x: 1050000, y: 2550000, w: 10100000, h: 2600000 }, [
|
|
1929
|
-
para(title || slide.text || '', { size: 3000, color: p.fg, bold: true, font }),
|
|
1930
|
-
]))
|
|
1931
|
-
if (subtitle !== '') {
|
|
1932
|
-
shapes.push(textShapeXml(idBase + 3, 'Attribution', { x: 1070000, y: 5300000, w: 9000000, h: 600000 }, [
|
|
1933
|
-
para('\u2014\u2014 ' + subtitle, { size: 1600, color: p.muted, font: theme.fonts.body }),
|
|
1934
|
-
]))
|
|
1935
|
-
}
|
|
1936
|
-
} else if (slide.layout === 'table') {
|
|
1937
|
-
const hasTitle = title !== ''
|
|
1938
|
-
if (kicker !== '' && kicker !== title) {
|
|
1939
|
-
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1940
|
-
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1941
|
-
]))
|
|
1942
|
-
}
|
|
1943
|
-
if (hasTitle) {
|
|
1944
|
-
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1945
|
-
para(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1946
|
-
]))
|
|
1947
|
-
}
|
|
1948
|
-
shapes.push(tableGraphicXml(idBase + 3, 'Table', { x: 900000, y: hasTitle ? 1950000 : 900000, w: 10300000 }, Array.isArray(slide.rows) ? slide.rows : [], theme, lang))
|
|
1949
|
-
} else if (slide.layout === 'closing') {
|
|
1950
|
-
shapes.push(textShapeXml(idBase + 1, 'Title', { x: 1050000, y: 2300000, w: 10200000, h: 1700000 }, [
|
|
1951
|
-
para(title || '谢谢', { size: 5200, color: p.fg, bold: true, align: 'ctr', font }),
|
|
1952
|
-
]))
|
|
1953
|
-
if (subtitle !== '') {
|
|
1954
|
-
shapes.push(textShapeXml(idBase + 2, 'Subtitle', { x: 1050000, y: 4200000, w: 10200000, h: 900000 }, [
|
|
1955
|
-
para(subtitle, { size: 2000, color: p.accent, bold: true, align: 'ctr', font: theme.fonts.body }),
|
|
1956
|
-
]))
|
|
1957
|
-
}
|
|
1958
|
-
} else {
|
|
1959
|
-
// bullets(默认布局)
|
|
1960
|
-
if (kicker !== '') {
|
|
1961
|
-
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1962
|
-
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1963
|
-
]))
|
|
1964
|
-
}
|
|
1965
|
-
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1966
|
-
para(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1967
|
-
]))
|
|
1968
|
-
const bodyParagraphs = bullets.length > 0
|
|
1969
|
-
? bullets.map((bullet) => para(bullet, { size: 2000, color: p.fg, bullet: true, font: theme.fonts.body, spaceAfter: 600 }))
|
|
1970
|
-
: [para(subtitle || '', { size: 2000, color: p.fg, font: theme.fonts.body })]
|
|
1971
|
-
shapes.push(textShapeXml(idBase + 3, 'Body', { x: 1050000, y: 1850000, w: 10100000, h: 4500000 }, bodyParagraphs))
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
shapes.push(textShapeXml(idBase + 9, 'Page number', { x: 10600000, y: 6250000, w: 1200000, h: 400000 }, [
|
|
1975
|
-
para(footer, { size: 1000, color: p.muted, align: 'r', font: theme.fonts.body }),
|
|
1976
|
-
]))
|
|
1977
|
-
|
|
1978
|
-
return shapes.join('')
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
/**
|
|
1982
|
-
* 原生逐条点击显现动画(PowerPoint「出现,按段落」的标准时序树):
|
|
1983
|
-
* bldP 把正文文本框标记为按段落构建,主序列里每条要点一个点击节点。
|
|
1984
|
-
*/
|
|
1985
|
-
function bulletTimingXml(spid, clicks) {
|
|
1986
|
-
let id = 2
|
|
1987
|
-
const nextId = () => { id += 1; return String(id) }
|
|
1988
|
-
const clickPars = []
|
|
1989
|
-
for (let i = 0; i < clicks; i += 1) {
|
|
1990
|
-
clickPars.push(
|
|
1991
|
-
'<p:par><p:cTn id="' + nextId() + '" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst>' +
|
|
1992
|
-
'<p:par><p:cTn id="' + nextId() + '" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect">' +
|
|
1993
|
-
'<p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>' +
|
|
1994
|
-
'<p:set><p:cBhvr><p:cTn id="' + nextId() + '" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>' +
|
|
1995
|
-
'<p:tgtEl><p:spTgt spid="' + spid + '"/></p:tgtEl>' +
|
|
1996
|
-
'<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr>' +
|
|
1997
|
-
'<p:to><p:strVal val="visible"/></p:to></p:set>' +
|
|
1998
|
-
'</p:childTnLst></p:cTn></p:par>' +
|
|
1999
|
-
'</p:childTnLst></p:cTn></p:par>',
|
|
2000
|
-
)
|
|
2001
|
-
}
|
|
2002
|
-
return '<p:timing><p:tnLst><p:par>' +
|
|
2003
|
-
'<p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst>' +
|
|
2004
|
-
'<p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst>' +
|
|
2005
|
-
clickPars.join('') +
|
|
2006
|
-
'</p:childTnLst></p:cTn>' +
|
|
2007
|
-
'<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>' +
|
|
2008
|
-
'<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>' +
|
|
2009
|
-
'</p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>' +
|
|
2010
|
-
'<p:bldLst><p:bldP spid="' + spid + '" grpId="0"/></p:bldLst></p:timing>'
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function slideXml(slide, index, total, theme, langId, motion) {
|
|
2014
|
-
const p = theme.palette
|
|
2015
|
-
let motionXml = ''
|
|
2016
|
-
if (motion) {
|
|
2017
|
-
motionXml = '<p:transition spd="med"><p:fade/></p:transition>'
|
|
2018
|
-
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
2019
|
-
if (slide.layout === 'bullets' && bullets.length >= 2) {
|
|
2020
|
-
motionXml += bulletTimingXml((index + 1) * 10 + 3, bullets.length)
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2024
|
-
'<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2025
|
-
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2026
|
-
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2027
|
-
'<p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="' + hex(p.bg) + '"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>' +
|
|
2028
|
-
'<p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2029
|
-
'<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>' +
|
|
2030
|
-
slideShapeList(slide, index, total, theme, langId) +
|
|
2031
|
-
'</p:spTree></p:cSld>' +
|
|
2032
|
-
'<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>' +
|
|
2033
|
-
motionXml +
|
|
2034
|
-
'</p:sld>'
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
function slideRelXml(hasNotes, slideIndex) {
|
|
2038
|
-
const notesRel = hasNotes
|
|
2039
|
-
? '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide' + slideIndex + '.xml"/>'
|
|
2040
|
-
: ''
|
|
2041
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2042
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2043
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
2044
|
-
notesRel +
|
|
2045
|
-
'</Relationships>'
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
function presentationXml(slideCount, hasNotesMaster) {
|
|
2049
|
-
const slideIds = Array.from({ length: slideCount }, (_, i) =>
|
|
2050
|
-
'<p:sldId id="' + (256 + i) + '" r:id="rId' + (i + 2) + '"/>').join('')
|
|
2051
|
-
const notesMasterId = hasNotesMaster
|
|
2052
|
-
? '<p:notesMasterIdLst><p:notesMasterId id="2147483649" r:id="rId' + (slideCount + 2) + '"/></p:notesMasterIdLst>'
|
|
2053
|
-
: ''
|
|
2054
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2055
|
-
'<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2056
|
-
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2057
|
-
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2058
|
-
'<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>' +
|
|
2059
|
-
'<p:sldIdLst>' + slideIds + '</p:sldIdLst>' +
|
|
2060
|
-
notesMasterId +
|
|
2061
|
-
'<p:sldSz cx="' + EMU_W + '" cy="' + EMU_H + '" type="screen16x9"/>' +
|
|
2062
|
-
'<p:notesSz cx="6858000" cy="9144000"/>' +
|
|
2063
|
-
'</p:presentation>'
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
function presentationRelXml(slideCount, hasNotesMaster) {
|
|
2067
|
-
const slideRels = Array.from({ length: slideCount }, (_, i) =>
|
|
2068
|
-
'<Relationship Id="rId' + (i + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + (i + 1) + '.xml"/>').join('')
|
|
2069
|
-
const notesMasterRel = hasNotesMaster
|
|
2070
|
-
? '<Relationship Id="rId' + (slideCount + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="notesMasters/notesMaster1.xml"/>'
|
|
2071
|
-
: ''
|
|
2072
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2073
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2074
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>' +
|
|
2075
|
-
slideRels +
|
|
2076
|
-
notesMasterRel +
|
|
2077
|
-
'</Relationships>'
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
function notesMasterXml() {
|
|
2081
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2082
|
-
'<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2083
|
-
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2084
|
-
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2085
|
-
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2086
|
-
'<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>' +
|
|
2087
|
-
'</p:spTree></p:cSld>' +
|
|
2088
|
-
'<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"/>' +
|
|
2089
|
-
'<p:notesStyle><a:lvl1pPr><a:defRPr sz="1200"/></a:lvl1pPr></p:notesStyle>' +
|
|
2090
|
-
'</p:notesMaster>'
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
function notesMasterRelXml() {
|
|
2094
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2095
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2096
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>' +
|
|
2097
|
-
'</Relationships>'
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
function notesSlideXml(notes, langId) {
|
|
2101
|
-
const lang = langId || 'zh-CN'
|
|
2102
|
-
const paragraphs = String(notes ?? '').split('\n')
|
|
2103
|
-
.filter((line) => line.trim() !== '')
|
|
2104
|
-
.map((line) => '<a:p><a:r><a:rPr lang="' + lang + '" sz="1200" dirty="0"/><a:t>' + escapeXml(line) + '</a:t></a:r></a:p>')
|
|
2105
|
-
.join('')
|
|
2106
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2107
|
-
'<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2108
|
-
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2109
|
-
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2110
|
-
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2111
|
-
'<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>' +
|
|
2112
|
-
'<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>' +
|
|
2113
|
-
'<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/>' +
|
|
2114
|
-
'<p:txBody><a:bodyPr/><a:lstStyle/>' + paragraphs + '</p:txBody></p:sp>' +
|
|
2115
|
-
'</p:spTree></p:cSld>' +
|
|
2116
|
-
'<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>' +
|
|
2117
|
-
'</p:notes>'
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
function notesSlideRelXml(slideIndex) {
|
|
2121
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2122
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2123
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide' + slideIndex + '.xml"/>' +
|
|
2124
|
-
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>' +
|
|
2125
|
-
'</Relationships>'
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
function slideMasterXml(theme) {
|
|
2129
|
-
const p = theme.palette
|
|
2130
|
-
const levels = Array.from({ length: 9 }, (_, i) => {
|
|
2131
|
-
const sz = Math.max(1100, 2000 - i * 100)
|
|
2132
|
-
return '<a:lvl' + (i + 1) + 'pPr marL="' + (342900 + i * 342900) + '" indent="' + (-342900 - i * 0) + '">' +
|
|
2133
|
-
'<a:defRPr sz="' + sz + '"><a:solidFill><a:srgbClr val="' + hex(p.fg) + '"/></a:solidFill><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/></a:defRPr></a:lvl' + (i + 1) + 'pPr>'
|
|
2134
|
-
}).join('')
|
|
2135
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2136
|
-
'<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2137
|
-
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2138
|
-
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2139
|
-
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2140
|
-
'<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>' +
|
|
2141
|
-
'</p:spTree></p:cSld>' +
|
|
2142
|
-
'<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"/>' +
|
|
2143
|
-
'<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>' +
|
|
2144
|
-
'<p:txStyles><p:titleStyle>' + levels + '</p:titleStyle><p:bodyStyle>' + levels + '</p:bodyStyle><p:otherStyle>' + levels + '</p:otherStyle></p:txStyles>' +
|
|
2145
|
-
'</p:sldMaster>'
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
function slideMasterRelXml() {
|
|
2149
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2150
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2151
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
2152
|
-
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>' +
|
|
2153
|
-
'</Relationships>'
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
function slideLayoutXml(theme) {
|
|
2157
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2158
|
-
'<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2159
|
-
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2160
|
-
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">' +
|
|
2161
|
-
'<p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2162
|
-
'<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>' +
|
|
2163
|
-
'</p:spTree></p:cSld>' +
|
|
2164
|
-
'<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>' +
|
|
2165
|
-
'</p:sldLayout>'
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
function slideLayoutRelXml() {
|
|
2169
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2170
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2171
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>' +
|
|
2172
|
-
'</Relationships>'
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
function themeXml(theme) {
|
|
2176
|
-
const p = theme.palette
|
|
2177
|
-
const color = (name, value) => '<a:' + name + '><a:srgbClr val="' + hex(value) + '"/></a:' + name + '>'
|
|
2178
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2179
|
-
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="dsh-ppt ' + escapeXml(theme.name.en) + '">' +
|
|
2180
|
-
'<a:themeElements>' +
|
|
2181
|
-
'<a:clrScheme name="dsh-ppt">' +
|
|
2182
|
-
color('dk1', p.fg) + color('lt1', p.bg) + color('dk2', p.muted) + color('lt2', p.panel) +
|
|
2183
|
-
color('accent1', p.accent) + color('accent2', p.accent2) + color('accent3', p.fg) +
|
|
2184
|
-
color('accent4', p.muted) + color('accent5', p.accent) + color('accent6', p.accent2) +
|
|
2185
|
-
color('hlink', p.accent) + color('folHlink', p.accent2) +
|
|
2186
|
-
'</a:clrScheme>' +
|
|
2187
|
-
'<a:fontScheme name="dsh-ppt">' +
|
|
2188
|
-
'<a:majorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.heading)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.heading)) + '"/><a:cs typeface=""/></a:majorFont>' +
|
|
2189
|
-
'<a:minorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/><a:cs typeface=""/></a:minorFont>' +
|
|
2190
|
-
'</a:fontScheme>' +
|
|
2191
|
-
'<a:fmtScheme name="dsh-ppt">' +
|
|
2192
|
-
'<a:fillStyleLst>' +
|
|
2193
|
-
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
2194
|
-
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
2195
|
-
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
2196
|
-
'</a:fillStyleLst>' +
|
|
2197
|
-
'<a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst>' +
|
|
2198
|
-
'<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>' +
|
|
2199
|
-
'<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>' +
|
|
2200
|
-
'</a:fmtScheme>' +
|
|
2201
|
-
'</a:themeElements>' +
|
|
2202
|
-
'<a:objectDefaults/><a:extraClrSchemeLst/>' +
|
|
2203
|
-
'</a:theme>'
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
function contentTypesXml(slideCount, noteIndexes) {
|
|
2207
|
-
const overrides = Array.from({ length: slideCount }, (_, i) =>
|
|
2208
|
-
'<Override PartName="/ppt/slides/slide' + (i + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>').join('')
|
|
2209
|
-
const notesOverrides = noteIndexes.map((index) =>
|
|
2210
|
-
'<Override PartName="/ppt/notesSlides/notesSlide' + (index + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"/>').join('')
|
|
2211
|
-
const notesMasterOverride = noteIndexes.length > 0
|
|
2212
|
-
? '<Override PartName="/ppt/notesMasters/notesMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"/>'
|
|
2213
|
-
: ''
|
|
2214
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2215
|
-
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
2216
|
-
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
2217
|
-
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
2218
|
-
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>' +
|
|
2219
|
-
'<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>' +
|
|
2220
|
-
'<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>' +
|
|
2221
|
-
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' +
|
|
2222
|
-
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
|
|
2223
|
-
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
|
|
2224
|
-
notesMasterOverride +
|
|
2225
|
-
overrides +
|
|
2226
|
-
notesOverrides +
|
|
2227
|
-
'</Types>'
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
function rootRelXml() {
|
|
2231
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2232
|
-
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2233
|
-
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>' +
|
|
2234
|
-
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>' +
|
|
2235
|
-
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' +
|
|
2236
|
-
'</Relationships>'
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
function coreXml(title) {
|
|
2240
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2241
|
-
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" ' +
|
|
2242
|
-
'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" ' +
|
|
2243
|
-
'xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
|
|
2244
|
-
'<dc:title>' + escapeXml(title) + '</dc:title><dc:creator>dsh-ppt</dc:creator>' +
|
|
2245
|
-
'<cp:lastModifiedBy>dsh-ppt</cp:lastModifiedBy>' +
|
|
2246
|
-
'</cp:coreProperties>'
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
function appXml(slideCount, notesCount) {
|
|
2250
|
-
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2251
|
-
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" ' +
|
|
2252
|
-
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">' +
|
|
2253
|
-
'<Application>dsh-ppt</Application><PresentationFormat>Widescreen</PresentationFormat>' +
|
|
2254
|
-
'<Slides>' + slideCount + '</Slides><Notes>' + notesCount + '</Notes><HiddenSlides>0</HiddenSlides>' +
|
|
2255
|
-
'</Properties>'
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
function buildZip(entries) {
|
|
2259
|
-
const chunks = []
|
|
2260
|
-
const central = []
|
|
2261
|
-
let offset = 0
|
|
2262
|
-
|
|
2263
|
-
for (const entry of entries) {
|
|
2264
|
-
const entryOffset = offset
|
|
2265
|
-
const name = Buffer.from(entry.name, 'utf8')
|
|
2266
|
-
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(String(entry.data), 'utf8')
|
|
2267
|
-
const crc = crc32(data)
|
|
2268
|
-
const compressed = deflateRawSync(data)
|
|
2269
|
-
const method = 8
|
|
2270
|
-
const nameLength = name.length
|
|
2271
|
-
const localHeader = Buffer.alloc(30)
|
|
2272
|
-
localHeader.writeUInt32LE(0x04034b50, 0)
|
|
2273
|
-
localHeader.writeUInt16LE(20, 4)
|
|
2274
|
-
localHeader.writeUInt16LE(0x0800, 6) // UTF-8 文件名
|
|
2275
|
-
localHeader.writeUInt16LE(method, 8)
|
|
2276
|
-
localHeader.writeUInt16LE(0, 10) // DOS time
|
|
2277
|
-
localHeader.writeUInt16LE(0x21, 12) // DOS date 1980-01-01
|
|
2278
|
-
localHeader.writeUInt32LE(crc, 14)
|
|
2279
|
-
localHeader.writeUInt32LE(compressed.length, 18)
|
|
2280
|
-
localHeader.writeUInt32LE(data.length, 22)
|
|
2281
|
-
localHeader.writeUInt16LE(nameLength, 26)
|
|
2282
|
-
localHeader.writeUInt16LE(0, 28)
|
|
2283
|
-
|
|
2284
|
-
chunks.push(localHeader, name, compressed)
|
|
2285
|
-
offset += 30 + nameLength + compressed.length
|
|
2286
|
-
|
|
2287
|
-
const centralHeader = Buffer.alloc(46)
|
|
2288
|
-
centralHeader.writeUInt32LE(0x02014b50, 0)
|
|
2289
|
-
centralHeader.writeUInt16LE(20, 4)
|
|
2290
|
-
centralHeader.writeUInt16LE(20, 6)
|
|
2291
|
-
centralHeader.writeUInt16LE(0x0800, 8)
|
|
2292
|
-
centralHeader.writeUInt16LE(method, 10)
|
|
2293
|
-
centralHeader.writeUInt16LE(0, 12)
|
|
2294
|
-
centralHeader.writeUInt16LE(0x21, 14)
|
|
2295
|
-
centralHeader.writeUInt32LE(crc, 16)
|
|
2296
|
-
centralHeader.writeUInt32LE(compressed.length, 20)
|
|
2297
|
-
centralHeader.writeUInt32LE(data.length, 24)
|
|
2298
|
-
centralHeader.writeUInt16LE(nameLength, 28)
|
|
2299
|
-
centralHeader.writeUInt16LE(0, 30)
|
|
2300
|
-
centralHeader.writeUInt16LE(0, 32)
|
|
2301
|
-
centralHeader.writeUInt16LE(0, 34)
|
|
2302
|
-
centralHeader.writeUInt16LE(0, 36)
|
|
2303
|
-
centralHeader.writeUInt32LE(0, 38)
|
|
2304
|
-
centralHeader.writeUInt32LE(entryOffset, 42)
|
|
2305
|
-
central.push(centralHeader, name)
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
const centralOffset = offset
|
|
2309
|
-
const centralSize = central.reduce((sum, part) => sum + part.length, 0)
|
|
2310
|
-
const end = Buffer.alloc(22)
|
|
2311
|
-
end.writeUInt32LE(0x06054b50, 0)
|
|
2312
|
-
end.writeUInt16LE(0, 4)
|
|
2313
|
-
end.writeUInt16LE(0, 6)
|
|
2314
|
-
end.writeUInt16LE(entries.length, 8)
|
|
2315
|
-
end.writeUInt16LE(entries.length, 10)
|
|
2316
|
-
end.writeUInt32LE(centralSize, 12)
|
|
2317
|
-
end.writeUInt32LE(centralOffset, 16)
|
|
2318
|
-
end.writeUInt16LE(0, 20)
|
|
2319
|
-
|
|
2320
|
-
chunks.push(...central, end)
|
|
2321
|
-
return Buffer.concat(chunks)
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
let CRC_TABLE = null
|
|
2325
|
-
function crc32(buffer) {
|
|
2326
|
-
if (CRC_TABLE === null) {
|
|
2327
|
-
CRC_TABLE = new Int32Array(256)
|
|
2328
|
-
for (let n = 0; n < 256; n += 1) {
|
|
2329
|
-
let c = n
|
|
2330
|
-
for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1)
|
|
2331
|
-
CRC_TABLE[n] = c
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
let crc = 0xffffffff
|
|
2335
|
-
for (let i = 0; i < buffer.length; i += 1) {
|
|
2336
|
-
crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8)
|
|
2337
|
-
}
|
|
2338
|
-
return (crc ^ 0xffffffff) >>> 0
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
export function buildPptx(manifest, themeInput, languageInput) {
|
|
2342
|
-
const theme = themeInput?.id ? themeInput : resolveTheme(themeInput)
|
|
2343
|
-
const language = languageInput?.id ? languageInput : resolveLanguage(languageInput)
|
|
2344
|
-
const motion = manifest.motion !== false
|
|
2345
|
-
const slides = manifest.slides
|
|
2346
|
-
const noteIndexes = []
|
|
2347
|
-
slides.forEach((slide, index) => {
|
|
2348
|
-
if (typeof slide.notes === 'string' && slide.notes.trim() !== '') noteIndexes.push(index)
|
|
2349
|
-
})
|
|
2350
|
-
const hasNotesMaster = noteIndexes.length > 0
|
|
2351
|
-
const entries = [
|
|
2352
|
-
{ name: '[Content_Types].xml', data: contentTypesXml(slides.length, noteIndexes) },
|
|
2353
|
-
{ name: '_rels/.rels', data: rootRelXml() },
|
|
2354
|
-
{ name: 'docProps/app.xml', data: appXml(slides.length, noteIndexes.length) },
|
|
2355
|
-
{ name: 'docProps/core.xml', data: coreXml(manifest.title) },
|
|
2356
|
-
{ name: 'ppt/presentation.xml', data: presentationXml(slides.length, hasNotesMaster) },
|
|
2357
|
-
{ name: 'ppt/_rels/presentation.xml.rels', data: presentationRelXml(slides.length, hasNotesMaster) },
|
|
2358
|
-
{ name: 'ppt/slideMasters/slideMaster1.xml', data: slideMasterXml(theme) },
|
|
2359
|
-
{ name: 'ppt/slideMasters/_rels/slideMaster1.xml.rels', data: slideMasterRelXml() },
|
|
2360
|
-
{ name: 'ppt/slideLayouts/slideLayout1.xml', data: slideLayoutXml(theme) },
|
|
2361
|
-
{ name: 'ppt/slideLayouts/_rels/slideLayout1.xml.rels', data: slideLayoutRelXml() },
|
|
2362
|
-
{ name: 'ppt/theme/theme1.xml', data: themeXml(theme) },
|
|
2363
|
-
]
|
|
2364
|
-
if (hasNotesMaster) {
|
|
2365
|
-
entries.push({ name: 'ppt/notesMasters/notesMaster1.xml', data: notesMasterXml() })
|
|
2366
|
-
entries.push({ name: 'ppt/notesMasters/_rels/notesMaster1.xml.rels', data: notesMasterRelXml() })
|
|
2367
|
-
}
|
|
2368
|
-
slides.forEach((slide, index) => {
|
|
2369
|
-
const hasNotes = noteIndexes.includes(index)
|
|
2370
|
-
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.attr, motion) })
|
|
2371
|
-
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml(hasNotes, index + 1) })
|
|
2372
|
-
if (hasNotes) {
|
|
2373
|
-
entries.push({ name: 'ppt/notesSlides/notesSlide' + (index + 1) + '.xml', data: notesSlideXml(slide.notes, language.attr) })
|
|
2374
|
-
entries.push({ name: 'ppt/notesSlides/_rels/notesSlide' + (index + 1) + '.xml.rels', data: notesSlideRelXml(index + 1) })
|
|
2375
|
-
}
|
|
2376
|
-
})
|
|
2377
|
-
return buildZip(entries)
|
|
2378
|
-
}
|
|
1652
|
+
go(Number.isInteger(start) ? start - 1 : 0);
|
|
1653
|
+
})();
|
|
1654
|
+
</script>
|
|
1655
|
+
</body>
|
|
1656
|
+
</html>`
|
|
1657
|
+
}
|
|
1658
|
+
function renderHtmlSlide(slide, index, ui, langId, manifest = {}, theme) {
|
|
1659
|
+
const kicker = slide.kicker || (slide.layout === 'cover' ? ui.coverKicker : '')
|
|
1660
|
+
const title = slide.title || ''
|
|
1661
|
+
const subtitle = slide.subtitle || ''
|
|
1662
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
1663
|
+
const number = String(index + 1).padStart(2, '0')
|
|
1664
|
+
let inner = ''
|
|
1665
|
+
switch (slide.layout) {
|
|
1666
|
+
case 'image':
|
|
1667
|
+
case 'image-left':
|
|
1668
|
+
case 'image-right':
|
|
1669
|
+
case 'chart':
|
|
1670
|
+
inner = renderMediaHtml(slide, manifest, theme)
|
|
1671
|
+
break
|
|
1672
|
+
case 'cover':
|
|
1673
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1674
|
+
'<h1>' + escapeHtml(title) + '</h1>' +
|
|
1675
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
1676
|
+
'<div class="meta">' + escapeHtml(ui.generatedBy) + '</div>'
|
|
1677
|
+
break
|
|
1678
|
+
case 'section':
|
|
1679
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1680
|
+
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
1681
|
+
'<div class="accent-line"></div>' +
|
|
1682
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
1683
|
+
break
|
|
1684
|
+
case 'statement':
|
|
1685
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1686
|
+
'<div class="statement-title">' + escapeHtml(title || slide.text || '') + '</div>' +
|
|
1687
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
1688
|
+
break
|
|
1689
|
+
case 'quote':
|
|
1690
|
+
inner = '<div class="kicker">' + escapeHtml(kicker || ui.quoteKicker) + '</div>' +
|
|
1691
|
+
'<div class="quote-mark" aria-hidden="true">\u201C</div>' +
|
|
1692
|
+
'<blockquote class="quote-text">' + escapeHtml(title || slide.text || '') + '</blockquote>' +
|
|
1693
|
+
(subtitle !== '' ? '<div class="quote-attr">\u2014\u2014 ' + escapeHtml(subtitle) + '</div>' : '')
|
|
1694
|
+
break
|
|
1695
|
+
case 'table': {
|
|
1696
|
+
const rows = Array.isArray(slide.rows) ? slide.rows : []
|
|
1697
|
+
const head = rows[0] ?? []
|
|
1698
|
+
const body = rows.slice(1)
|
|
1699
|
+
const isNum = (cell) => /^\s*[+-]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?\s*(?:%|%|次|页|个|人|元|天|小时|分钟)?\s*$/.test(String(cell ?? ''))
|
|
1700
|
+
const headingText = title !== '' ? title : (kicker || ui.tableKicker)
|
|
1701
|
+
const showKicker = kicker !== '' && kicker !== headingText
|
|
1702
|
+
inner = (showKicker ? '<div class="kicker">' + escapeHtml(kicker) + '</div>' : '') +
|
|
1703
|
+
'<h2>' + escapeHtml(headingText) + '</h2>' +
|
|
1704
|
+
'<table class="deck-table"><thead><tr>' +
|
|
1705
|
+
head.map((cell) => '<th' + (isNum(cell) ? ' class="num"' : '') + '>' + escapeHtml(cell) + '</th>').join('') +
|
|
1706
|
+
'</tr></thead><tbody>' +
|
|
1707
|
+
body.map((row) => '<tr>' + row.map((cell) => '<td' + (isNum(cell) ? ' class="num"' : '') + '>' + escapeHtml(cell) + '</td>').join('') + '</tr>').join('') +
|
|
1708
|
+
'</tbody></table>'
|
|
1709
|
+
break
|
|
1710
|
+
}
|
|
1711
|
+
case 'closing':
|
|
1712
|
+
inner = '<h1>' + escapeHtml(title || ui.closingTitle) + '</h1>' +
|
|
1713
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
1714
|
+
'<div class="meta">' + escapeHtml(ui.generatedBy) + '</div>'
|
|
1715
|
+
break
|
|
1716
|
+
case 'bullets':
|
|
1717
|
+
default:
|
|
1718
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1719
|
+
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
1720
|
+
'<ul>' + bullets.map((bullet, bulletIndex) => '<li style="--i:' + bulletIndex + '">' + escapeHtml(bullet) + '</li>').join('') + '</ul>'
|
|
1721
|
+
break
|
|
1722
|
+
}
|
|
1723
|
+
const notesAttr = typeof slide.notes === 'string' && slide.notes.trim() !== ''
|
|
1724
|
+
? ' data-notes="' + escapeHtml(slide.notes) + '"'
|
|
1725
|
+
: ''
|
|
1726
|
+
return '<section class="slide slide--' + escapeHtml(slide.layout || 'bullets') + '" data-index="' + number + '"' + notesAttr + '>' +
|
|
1727
|
+
'<div class="slide-inner">' + inner + '</div>' +
|
|
1728
|
+
(manifest.brand?.logoAssetId ? '<img class="brand-logo" src="' + escapeHtml(manifest.assets?.[manifest.brand.logoAssetId]?.data ?? '') + '" alt="' + escapeHtml(manifest.brand.name || 'Logo') + '"/>' : '') +
|
|
1729
|
+
(manifest.brand?.footer ? '<div class="brand-footer">' + escapeHtml(manifest.brand.footer) + '</div>' : '') + '</section>'
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function hexToRgba(hex, alpha) {
|
|
1733
|
+
const value = String(hex).replace('#', '')
|
|
1734
|
+
const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value
|
|
1735
|
+
const num = Number.parseInt(full, 16)
|
|
1736
|
+
const r = (num >> 16) & 255
|
|
1737
|
+
const g = (num >> 8) & 255
|
|
1738
|
+
const b = num & 255
|
|
1739
|
+
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// ---------------------------------------------------------------------------
|
|
1743
|
+
// PPTX(OOXML 手写 + node:zlib zip)
|
|
1744
|
+
// ---------------------------------------------------------------------------
|
|
1745
|
+
|
|
1746
|
+
const EMU_W = 12192000
|
|
1747
|
+
const EMU_H = 6858000
|
|
1748
|
+
|
|
1749
|
+
function hex(value) {
|
|
1750
|
+
return String(value).replace('#', '').toUpperCase()
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
export function pptxFontName(value) {
|
|
1754
|
+
const names = String(value ?? '').split(',').map(name => name.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
|
|
1755
|
+
// A CSS stack may start with an optional web font. Office receives one actual
|
|
1756
|
+
// family, so select a platform fallback instead of copying the first name.
|
|
1757
|
+
if (names.length === 1 && !['serif', 'sans-serif', 'system-ui'].includes(names[0])) return names[0]
|
|
1758
|
+
const available = process.platform === 'win32'
|
|
1759
|
+
? ['Segoe UI', 'Arial', 'Arial Black', 'Impact', 'Georgia', 'Times New Roman', 'Microsoft YaHei', 'SimSun']
|
|
1760
|
+
: process.platform === 'darwin'
|
|
1761
|
+
? ['Helvetica Neue', 'Arial', 'Impact', 'Georgia', 'Times New Roman', 'Avenir Next', 'PingFang SC', 'Songti SC']
|
|
1762
|
+
: ['Liberation Sans', 'Liberation Serif', 'Noto Sans', 'Noto Sans CJK SC', 'Noto Serif CJK SC']
|
|
1763
|
+
return names.find(name => available.includes(name)) || (names.includes('serif')
|
|
1764
|
+
? process.platform === 'linux' ? 'Liberation Serif' : 'Georgia'
|
|
1765
|
+
: process.platform === 'win32' ? 'Segoe UI' : process.platform === 'darwin' ? 'Helvetica Neue' : 'Liberation Sans')
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
export function pptxEastAsianFont(value) {
|
|
1769
|
+
const font = pptxFontName(value)
|
|
1770
|
+
if (/Microsoft YaHei|SimSun|SimHei|PingFang|Songti|Heiti|Noto (Sans|Serif) CJK|[\u3400-\u9fff]/i.test(font)) return font
|
|
1771
|
+
const serif = /Georgia|Times New Roman|Cambria|Liberation Serif/i.test(font)
|
|
1772
|
+
return process.platform === 'win32' ? serif ? 'SimSun' : 'Microsoft YaHei'
|
|
1773
|
+
: process.platform === 'darwin' ? serif ? 'Songti SC' : 'PingFang SC'
|
|
1774
|
+
: serif ? 'Noto Serif CJK SC' : 'Noto Sans CJK SC'
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function paragraphXml(text, options = {}) {
|
|
1778
|
+
const {
|
|
1779
|
+
size = 2000,
|
|
1780
|
+
color = 'FFFFFF',
|
|
1781
|
+
bold = false,
|
|
1782
|
+
lang = 'zh-CN',
|
|
1783
|
+
align = 'l',
|
|
1784
|
+
bullet = false,
|
|
1785
|
+
font = 'Arial',
|
|
1786
|
+
spaceBefore = 0,
|
|
1787
|
+
spaceAfter = 0,
|
|
1788
|
+
} = options
|
|
1789
|
+
const runFont = pptxFontName(font)
|
|
1790
|
+
const spacing = (spaceBefore > 0 ? '<a:spcBef><a:spcPts val="' + spaceBefore + '"/></a:spcBef>' : '') +
|
|
1791
|
+
(spaceAfter > 0 ? '<a:spcAft><a:spcPts val="' + spaceAfter + '"/></a:spcAft>' : '')
|
|
1792
|
+
const pPr = '<a:pPr' + (bullet ? ' marL="285750" indent="-285750"' : align !== 'l' ? ' algn="' + align + '"' : '') + '>' + spacing +
|
|
1793
|
+
(bullet ? '<a:buFont typeface="Arial" panose="020B0604020202020204"/><a:buChar char="•"/>' : '<a:buNone/>') + '</a:pPr>'
|
|
1794
|
+
return '<a:p>' + pPr +
|
|
1795
|
+
'<a:r><a:rPr lang="' + lang + '" sz="' + size + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1796
|
+
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1797
|
+
'<a:latin typeface="' + escapeXml(runFont) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(font)) + '"/></a:rPr>' +
|
|
1798
|
+
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p>'
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function textShapeXml(id, name, box, paragraphs, options = {}) {
|
|
1802
|
+
const { x = 0, y = 0, w = EMU_W, h = EMU_H } = box
|
|
1803
|
+
return '<p:sp>' +
|
|
1804
|
+
'<p:nvSpPr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
|
|
1805
|
+
'<p:spPr><a:xfrm><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + w + '" cy="' + h + '"/></a:xfrm>' +
|
|
1806
|
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln/></p:spPr>' +
|
|
1807
|
+
'<p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:normAutofit/></a:bodyPr><a:lstStyle/>' +
|
|
1808
|
+
paragraphs.join('') + '</p:txBody></p:sp>'
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function accentBarXml(id, name, box, color) {
|
|
1812
|
+
const { x = 0, y = 0, w = EMU_W, h = EMU_H } = box
|
|
1813
|
+
return '<p:sp>' +
|
|
1814
|
+
'<p:nvSpPr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
|
|
1815
|
+
'<p:spPr><a:xfrm><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + w + '" cy="' + h + '"/></a:xfrm>' +
|
|
1816
|
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' +
|
|
1817
|
+
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill><a:ln/></p:spPr>' +
|
|
1818
|
+
'<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>'
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
/** 原生 OOXML 表格(graphicFrame + a:tbl):首行主题色表头,隔行面板色。 */
|
|
1822
|
+
function tableGraphicXml(id, name, box, rows, theme, langId) {
|
|
1823
|
+
const p = theme.palette
|
|
1824
|
+
const lang = langId || 'zh-CN'
|
|
1825
|
+
const font = pptxFontName(theme.fonts.body)
|
|
1826
|
+
const safeRows = rows.length > 0 ? rows : [['']]
|
|
1827
|
+
const cols = Math.max(1, safeRows[0].length)
|
|
1828
|
+
const fontSize = cols <= 6 ? 1700 : 1400
|
|
1829
|
+
const colW = Math.floor(box.w / cols)
|
|
1830
|
+
const grid = Array.from({ length: cols }, () => '<a:gridCol w="' + colW + '"/>').join('')
|
|
1831
|
+
const rowH = 420000
|
|
1832
|
+
const cellXml = (text, fill, color, bold) =>
|
|
1833
|
+
'<a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="l"><a:buNone/></a:pPr>' +
|
|
1834
|
+
'<a:r><a:rPr lang="' + lang + '" sz="' + fontSize + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1835
|
+
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1836
|
+
'<a:latin typeface="' + escapeXml(font) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/></a:rPr>' +
|
|
1837
|
+
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p></a:txBody>' +
|
|
1838
|
+
'<a:tcPr marL="91440" marR="91440" anchor="ctr">' +
|
|
1839
|
+
(fill !== '' ? '<a:solidFill><a:srgbClr val="' + hex(fill) + '"/></a:solidFill>' : '<a:noFill/>') +
|
|
1840
|
+
'</a:tcPr></a:tc>'
|
|
1841
|
+
const trs = safeRows.map((row, rowIndex) => {
|
|
1842
|
+
const padded = Array.from({ length: cols }, (_, i) => row[i] ?? '')
|
|
1843
|
+
const cells = rowIndex === 0
|
|
1844
|
+
? padded.map((c) => cellXml(c, p.accent, p.bg, true)).join('')
|
|
1845
|
+
: padded.map((c) => cellXml(c, rowIndex % 2 === 1 ? p.panel : '', p.fg, false)).join('')
|
|
1846
|
+
return '<a:tr h="' + rowH + '">' + cells + '</a:tr>'
|
|
1847
|
+
}).join('')
|
|
1848
|
+
const height = Math.max(1, safeRows.length) * rowH
|
|
1849
|
+
return '<p:graphicFrame>' +
|
|
1850
|
+
'<p:nvGraphicFramePr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/>' +
|
|
1851
|
+
'<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr>' +
|
|
1852
|
+
'<p:xfrm><a:off x="' + box.x + '" y="' + box.y + '"/><a:ext cx="' + box.w + '" cy="' + height + '"/></p:xfrm>' +
|
|
1853
|
+
'<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">' +
|
|
1854
|
+
'<a:tbl><a:tblPr firstRow="1" bandRow="1"/>' +
|
|
1855
|
+
'<a:tblGrid>' + grid + '</a:tblGrid>' + trs + '</a:tbl>' +
|
|
1856
|
+
'</a:graphicData></a:graphic></p:graphicFrame>'
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function slideShapeList(slide, index, total, theme, langId) {
|
|
1860
|
+
const p = theme.palette
|
|
1861
|
+
const font = pptxFontName(theme.fonts.heading)
|
|
1862
|
+
const bodyFont = pptxFontName(theme.fonts.body) // 保留:v0.1 后续用于统一正文字体
|
|
1863
|
+
const lang = langId || 'zh-CN'
|
|
1864
|
+
// PPTX 每个文本 run 的语言跟随 deck.lang(zh-CN / en-US),不再写死中文
|
|
1865
|
+
const para = (content, options = {}) => paragraphXml(content, { lang, ...options })
|
|
1866
|
+
const shapes = []
|
|
1867
|
+
const kicker = slide.kicker || ''
|
|
1868
|
+
const title = slide.title || ''
|
|
1869
|
+
const subtitle = slide.subtitle || ''
|
|
1870
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
1871
|
+
const idBase = (index + 1) * 10
|
|
1872
|
+
const footer = String(index + 1).padStart(2, '0') + ' / ' + String(total).padStart(2, '0')
|
|
1873
|
+
|
|
1874
|
+
if (MEDIA_LAYOUTS.has(slide.layout)) {
|
|
1875
|
+
const boxes = mediaBoxes(slide.layout)
|
|
1876
|
+
const emu = box => Object.fromEntries(Object.entries(box).map(([key, value]) => [key, Math.round(value * 914400)]))
|
|
1877
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', emu(boxes.title), [para(title, { size: 3400, color: p.fg, bold: true, font })]))
|
|
1878
|
+
if (boxes.body) shapes.push(textShapeXml(idBase + 3, 'Body', emu(boxes.body), [
|
|
1879
|
+
...(subtitle ? [para(subtitle, { size: 2000, color: p.fg, font: bodyFont })] : []),
|
|
1880
|
+
...bullets.map(bullet => para(bullet, { size: 2000, color: p.fg, bullet: true, font: bodyFont, spaceAfter: 600 })),
|
|
1881
|
+
]))
|
|
1882
|
+
const caption = slide.chart?.caption || slide.image?.caption
|
|
1883
|
+
if (caption) shapes.push(textShapeXml(idBase + 4, 'Caption', emu(boxes.caption), [para(caption, { size: 1400, color: p.muted, font: bodyFont })]))
|
|
1884
|
+
} else if (slide.layout === 'cover') {
|
|
1885
|
+
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 2250000, w: 240000, h: 1600000 }, p.accent))
|
|
1886
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 1550000, y: 2160000, w: 9250000, h: 1800000 }, [
|
|
1887
|
+
para(title, { size: 4400, color: p.fg, bold: true, font }),
|
|
1888
|
+
]))
|
|
1889
|
+
if (subtitle !== '') {
|
|
1890
|
+
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1570000, y: 4150000, w: 9000000, h: 1200000 }, [
|
|
1891
|
+
para(subtitle, { size: 2200, color: p.muted, font: theme.fonts.body }),
|
|
1892
|
+
]))
|
|
1893
|
+
}
|
|
1894
|
+
if (kicker !== '') {
|
|
1895
|
+
shapes.push(textShapeXml(idBase + 4, 'Kicker', { x: 1570000, y: 5800000, w: 7000000, h: 500000 }, [
|
|
1896
|
+
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1897
|
+
]))
|
|
1898
|
+
}
|
|
1899
|
+
} else if (slide.layout === 'section') {
|
|
1900
|
+
if (kicker !== '') {
|
|
1901
|
+
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 1050000, y: 2250000, w: 9000000, h: 500000 }, [
|
|
1902
|
+
para(kicker, { size: 1600, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1903
|
+
]))
|
|
1904
|
+
}
|
|
1905
|
+
shapes.push(accentBarXml(idBase + 2, 'Accent bar', { x: 1050000, y: 2850000, w: 1600000, h: 160000 }, p.accent2))
|
|
1906
|
+
shapes.push(textShapeXml(idBase + 3, 'Title', { x: 1050000, y: 3150000, w: 10200000, h: 1400000 }, [
|
|
1907
|
+
para(title, { size: 4000, color: p.fg, bold: true, font }),
|
|
1908
|
+
]))
|
|
1909
|
+
if (subtitle !== '') {
|
|
1910
|
+
shapes.push(textShapeXml(idBase + 4, 'Subtitle', { x: 1070000, y: 4750000, w: 9000000, h: 900000 }, [
|
|
1911
|
+
para(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
1912
|
+
]))
|
|
1913
|
+
}
|
|
1914
|
+
} else if (slide.layout === 'statement') {
|
|
1915
|
+
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 1900000, w: 200000, h: 2800000 }, p.accent))
|
|
1916
|
+
shapes.push(textShapeXml(idBase + 2, 'Statement', { x: 1500000, y: 1950000, w: 9400000, h: 2700000 }, [
|
|
1917
|
+
para(title || slide.text || '', { size: 3600, color: p.fg, bold: true, font }),
|
|
1918
|
+
]))
|
|
1919
|
+
if (subtitle !== '') {
|
|
1920
|
+
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1520000, y: 4900000, w: 9000000, h: 800000 }, [
|
|
1921
|
+
para(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
1922
|
+
]))
|
|
1923
|
+
}
|
|
1924
|
+
} else if (slide.layout === 'quote') {
|
|
1925
|
+
shapes.push(textShapeXml(idBase + 1, 'Quote mark', { x: 900000, y: 1050000, w: 1700000, h: 1900000 }, [
|
|
1926
|
+
para('\u201C', { size: 9600, color: p.accent, bold: true, font }),
|
|
1927
|
+
]))
|
|
1928
|
+
shapes.push(textShapeXml(idBase + 2, 'Quote', { x: 1050000, y: 2550000, w: 10100000, h: 2600000 }, [
|
|
1929
|
+
para(title || slide.text || '', { size: 3000, color: p.fg, bold: true, font }),
|
|
1930
|
+
]))
|
|
1931
|
+
if (subtitle !== '') {
|
|
1932
|
+
shapes.push(textShapeXml(idBase + 3, 'Attribution', { x: 1070000, y: 5300000, w: 9000000, h: 600000 }, [
|
|
1933
|
+
para('\u2014\u2014 ' + subtitle, { size: 1600, color: p.muted, font: theme.fonts.body }),
|
|
1934
|
+
]))
|
|
1935
|
+
}
|
|
1936
|
+
} else if (slide.layout === 'table') {
|
|
1937
|
+
const hasTitle = title !== ''
|
|
1938
|
+
if (kicker !== '' && kicker !== title) {
|
|
1939
|
+
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1940
|
+
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1941
|
+
]))
|
|
1942
|
+
}
|
|
1943
|
+
if (hasTitle) {
|
|
1944
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1945
|
+
para(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1946
|
+
]))
|
|
1947
|
+
}
|
|
1948
|
+
shapes.push(tableGraphicXml(idBase + 3, 'Table', { x: 900000, y: hasTitle ? 1950000 : 900000, w: 10300000 }, Array.isArray(slide.rows) ? slide.rows : [], theme, lang))
|
|
1949
|
+
} else if (slide.layout === 'closing') {
|
|
1950
|
+
shapes.push(textShapeXml(idBase + 1, 'Title', { x: 1050000, y: 2300000, w: 10200000, h: 1700000 }, [
|
|
1951
|
+
para(title || '谢谢', { size: 5200, color: p.fg, bold: true, align: 'ctr', font }),
|
|
1952
|
+
]))
|
|
1953
|
+
if (subtitle !== '') {
|
|
1954
|
+
shapes.push(textShapeXml(idBase + 2, 'Subtitle', { x: 1050000, y: 4200000, w: 10200000, h: 900000 }, [
|
|
1955
|
+
para(subtitle, { size: 2000, color: p.accent, bold: true, align: 'ctr', font: theme.fonts.body }),
|
|
1956
|
+
]))
|
|
1957
|
+
}
|
|
1958
|
+
} else {
|
|
1959
|
+
// bullets(默认布局)
|
|
1960
|
+
if (kicker !== '') {
|
|
1961
|
+
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1962
|
+
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1963
|
+
]))
|
|
1964
|
+
}
|
|
1965
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
1966
|
+
para(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
1967
|
+
]))
|
|
1968
|
+
const bodyParagraphs = bullets.length > 0
|
|
1969
|
+
? bullets.map((bullet) => para(bullet, { size: 2000, color: p.fg, bullet: true, font: theme.fonts.body, spaceAfter: 600 }))
|
|
1970
|
+
: [para(subtitle || '', { size: 2000, color: p.fg, font: theme.fonts.body })]
|
|
1971
|
+
shapes.push(textShapeXml(idBase + 3, 'Body', { x: 1050000, y: 1850000, w: 10100000, h: 4500000 }, bodyParagraphs))
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
shapes.push(textShapeXml(idBase + 9, 'Page number', { x: 10600000, y: 6250000, w: 1200000, h: 400000 }, [
|
|
1975
|
+
para(footer, { size: 1000, color: p.muted, align: 'r', font: theme.fonts.body }),
|
|
1976
|
+
]))
|
|
1977
|
+
|
|
1978
|
+
return shapes.join('')
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* 原生逐条点击显现动画(PowerPoint「出现,按段落」的标准时序树):
|
|
1983
|
+
* bldP 把正文文本框标记为按段落构建,主序列里每条要点一个点击节点。
|
|
1984
|
+
*/
|
|
1985
|
+
function bulletTimingXml(spid, clicks) {
|
|
1986
|
+
let id = 2
|
|
1987
|
+
const nextId = () => { id += 1; return String(id) }
|
|
1988
|
+
const clickPars = []
|
|
1989
|
+
for (let i = 0; i < clicks; i += 1) {
|
|
1990
|
+
clickPars.push(
|
|
1991
|
+
'<p:par><p:cTn id="' + nextId() + '" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst>' +
|
|
1992
|
+
'<p:par><p:cTn id="' + nextId() + '" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect">' +
|
|
1993
|
+
'<p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>' +
|
|
1994
|
+
'<p:set><p:cBhvr><p:cTn id="' + nextId() + '" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>' +
|
|
1995
|
+
'<p:tgtEl><p:spTgt spid="' + spid + '"/></p:tgtEl>' +
|
|
1996
|
+
'<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr>' +
|
|
1997
|
+
'<p:to><p:strVal val="visible"/></p:to></p:set>' +
|
|
1998
|
+
'</p:childTnLst></p:cTn></p:par>' +
|
|
1999
|
+
'</p:childTnLst></p:cTn></p:par>',
|
|
2000
|
+
)
|
|
2001
|
+
}
|
|
2002
|
+
return '<p:timing><p:tnLst><p:par>' +
|
|
2003
|
+
'<p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst>' +
|
|
2004
|
+
'<p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst>' +
|
|
2005
|
+
clickPars.join('') +
|
|
2006
|
+
'</p:childTnLst></p:cTn>' +
|
|
2007
|
+
'<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>' +
|
|
2008
|
+
'<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>' +
|
|
2009
|
+
'</p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>' +
|
|
2010
|
+
'<p:bldLst><p:bldP spid="' + spid + '" grpId="0"/></p:bldLst></p:timing>'
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function slideXml(slide, index, total, theme, langId, motion) {
|
|
2014
|
+
const p = theme.palette
|
|
2015
|
+
let motionXml = ''
|
|
2016
|
+
if (motion) {
|
|
2017
|
+
motionXml = '<p:transition spd="med"><p:fade/></p:transition>'
|
|
2018
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
2019
|
+
if (slide.layout === 'bullets' && bullets.length >= 2) {
|
|
2020
|
+
motionXml += bulletTimingXml((index + 1) * 10 + 3, bullets.length)
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2024
|
+
'<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2025
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2026
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2027
|
+
'<p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="' + hex(p.bg) + '"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>' +
|
|
2028
|
+
'<p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2029
|
+
'<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>' +
|
|
2030
|
+
slideShapeList(slide, index, total, theme, langId) +
|
|
2031
|
+
'</p:spTree></p:cSld>' +
|
|
2032
|
+
'<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>' +
|
|
2033
|
+
motionXml +
|
|
2034
|
+
'</p:sld>'
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function slideRelXml(hasNotes, slideIndex) {
|
|
2038
|
+
const notesRel = hasNotes
|
|
2039
|
+
? '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide' + slideIndex + '.xml"/>'
|
|
2040
|
+
: ''
|
|
2041
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2042
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2043
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
2044
|
+
notesRel +
|
|
2045
|
+
'</Relationships>'
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function presentationXml(slideCount, hasNotesMaster) {
|
|
2049
|
+
const slideIds = Array.from({ length: slideCount }, (_, i) =>
|
|
2050
|
+
'<p:sldId id="' + (256 + i) + '" r:id="rId' + (i + 2) + '"/>').join('')
|
|
2051
|
+
const notesMasterId = hasNotesMaster
|
|
2052
|
+
? '<p:notesMasterIdLst><p:notesMasterId id="2147483649" r:id="rId' + (slideCount + 2) + '"/></p:notesMasterIdLst>'
|
|
2053
|
+
: ''
|
|
2054
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2055
|
+
'<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2056
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2057
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2058
|
+
'<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>' +
|
|
2059
|
+
'<p:sldIdLst>' + slideIds + '</p:sldIdLst>' +
|
|
2060
|
+
notesMasterId +
|
|
2061
|
+
'<p:sldSz cx="' + EMU_W + '" cy="' + EMU_H + '" type="screen16x9"/>' +
|
|
2062
|
+
'<p:notesSz cx="6858000" cy="9144000"/>' +
|
|
2063
|
+
'</p:presentation>'
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function presentationRelXml(slideCount, hasNotesMaster) {
|
|
2067
|
+
const slideRels = Array.from({ length: slideCount }, (_, i) =>
|
|
2068
|
+
'<Relationship Id="rId' + (i + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + (i + 1) + '.xml"/>').join('')
|
|
2069
|
+
const notesMasterRel = hasNotesMaster
|
|
2070
|
+
? '<Relationship Id="rId' + (slideCount + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="notesMasters/notesMaster1.xml"/>'
|
|
2071
|
+
: ''
|
|
2072
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2073
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2074
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>' +
|
|
2075
|
+
slideRels +
|
|
2076
|
+
notesMasterRel +
|
|
2077
|
+
'</Relationships>'
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function notesMasterXml() {
|
|
2081
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2082
|
+
'<p:notesMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2083
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2084
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2085
|
+
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2086
|
+
'<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>' +
|
|
2087
|
+
'</p:spTree></p:cSld>' +
|
|
2088
|
+
'<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"/>' +
|
|
2089
|
+
'<p:notesStyle><a:lvl1pPr><a:defRPr sz="1200"/></a:lvl1pPr></p:notesStyle>' +
|
|
2090
|
+
'</p:notesMaster>'
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function notesMasterRelXml() {
|
|
2094
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2095
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2096
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>' +
|
|
2097
|
+
'</Relationships>'
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
function notesSlideXml(notes, langId) {
|
|
2101
|
+
const lang = langId || 'zh-CN'
|
|
2102
|
+
const paragraphs = String(notes ?? '').split('\n')
|
|
2103
|
+
.filter((line) => line.trim() !== '')
|
|
2104
|
+
.map((line) => '<a:p><a:r><a:rPr lang="' + lang + '" sz="1200" dirty="0"/><a:t>' + escapeXml(line) + '</a:t></a:r></a:p>')
|
|
2105
|
+
.join('')
|
|
2106
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2107
|
+
'<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2108
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2109
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2110
|
+
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2111
|
+
'<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>' +
|
|
2112
|
+
'<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>' +
|
|
2113
|
+
'<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/>' +
|
|
2114
|
+
'<p:txBody><a:bodyPr/><a:lstStyle/>' + paragraphs + '</p:txBody></p:sp>' +
|
|
2115
|
+
'</p:spTree></p:cSld>' +
|
|
2116
|
+
'<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>' +
|
|
2117
|
+
'</p:notes>'
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
function notesSlideRelXml(slideIndex) {
|
|
2121
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2122
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2123
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide' + slideIndex + '.xml"/>' +
|
|
2124
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" Target="../notesMasters/notesMaster1.xml"/>' +
|
|
2125
|
+
'</Relationships>'
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
function slideMasterXml(theme) {
|
|
2129
|
+
const p = theme.palette
|
|
2130
|
+
const levels = Array.from({ length: 9 }, (_, i) => {
|
|
2131
|
+
const sz = Math.max(1100, 2000 - i * 100)
|
|
2132
|
+
return '<a:lvl' + (i + 1) + 'pPr marL="' + (342900 + i * 342900) + '" indent="' + (-342900 - i * 0) + '">' +
|
|
2133
|
+
'<a:defRPr sz="' + sz + '"><a:solidFill><a:srgbClr val="' + hex(p.fg) + '"/></a:solidFill><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/></a:defRPr></a:lvl' + (i + 1) + 'pPr>'
|
|
2134
|
+
}).join('')
|
|
2135
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2136
|
+
'<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2137
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2138
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
2139
|
+
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2140
|
+
'<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>' +
|
|
2141
|
+
'</p:spTree></p:cSld>' +
|
|
2142
|
+
'<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"/>' +
|
|
2143
|
+
'<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>' +
|
|
2144
|
+
'<p:txStyles><p:titleStyle>' + levels + '</p:titleStyle><p:bodyStyle>' + levels + '</p:bodyStyle><p:otherStyle>' + levels + '</p:otherStyle></p:txStyles>' +
|
|
2145
|
+
'</p:sldMaster>'
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function slideMasterRelXml() {
|
|
2149
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2150
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2151
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
2152
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>' +
|
|
2153
|
+
'</Relationships>'
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function slideLayoutXml(theme) {
|
|
2157
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2158
|
+
'<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
2159
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
2160
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">' +
|
|
2161
|
+
'<p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
2162
|
+
'<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>' +
|
|
2163
|
+
'</p:spTree></p:cSld>' +
|
|
2164
|
+
'<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>' +
|
|
2165
|
+
'</p:sldLayout>'
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function slideLayoutRelXml() {
|
|
2169
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2170
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2171
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>' +
|
|
2172
|
+
'</Relationships>'
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function themeXml(theme) {
|
|
2176
|
+
const p = theme.palette
|
|
2177
|
+
const color = (name, value) => '<a:' + name + '><a:srgbClr val="' + hex(value) + '"/></a:' + name + '>'
|
|
2178
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2179
|
+
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="dsh-ppt ' + escapeXml(theme.name.en) + '">' +
|
|
2180
|
+
'<a:themeElements>' +
|
|
2181
|
+
'<a:clrScheme name="dsh-ppt">' +
|
|
2182
|
+
color('dk1', p.fg) + color('lt1', p.bg) + color('dk2', p.muted) + color('lt2', p.panel) +
|
|
2183
|
+
color('accent1', p.accent) + color('accent2', p.accent2) + color('accent3', p.fg) +
|
|
2184
|
+
color('accent4', p.muted) + color('accent5', p.accent) + color('accent6', p.accent2) +
|
|
2185
|
+
color('hlink', p.accent) + color('folHlink', p.accent2) +
|
|
2186
|
+
'</a:clrScheme>' +
|
|
2187
|
+
'<a:fontScheme name="dsh-ppt">' +
|
|
2188
|
+
'<a:majorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.heading)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.heading)) + '"/><a:cs typeface=""/></a:majorFont>' +
|
|
2189
|
+
'<a:minorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/><a:cs typeface=""/></a:minorFont>' +
|
|
2190
|
+
'</a:fontScheme>' +
|
|
2191
|
+
'<a:fmtScheme name="dsh-ppt">' +
|
|
2192
|
+
'<a:fillStyleLst>' +
|
|
2193
|
+
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
2194
|
+
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
2195
|
+
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
2196
|
+
'</a:fillStyleLst>' +
|
|
2197
|
+
'<a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst>' +
|
|
2198
|
+
'<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>' +
|
|
2199
|
+
'<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst>' +
|
|
2200
|
+
'</a:fmtScheme>' +
|
|
2201
|
+
'</a:themeElements>' +
|
|
2202
|
+
'<a:objectDefaults/><a:extraClrSchemeLst/>' +
|
|
2203
|
+
'</a:theme>'
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function contentTypesXml(slideCount, noteIndexes) {
|
|
2207
|
+
const overrides = Array.from({ length: slideCount }, (_, i) =>
|
|
2208
|
+
'<Override PartName="/ppt/slides/slide' + (i + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>').join('')
|
|
2209
|
+
const notesOverrides = noteIndexes.map((index) =>
|
|
2210
|
+
'<Override PartName="/ppt/notesSlides/notesSlide' + (index + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"/>').join('')
|
|
2211
|
+
const notesMasterOverride = noteIndexes.length > 0
|
|
2212
|
+
? '<Override PartName="/ppt/notesMasters/notesMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"/>'
|
|
2213
|
+
: ''
|
|
2214
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2215
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
2216
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
2217
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
2218
|
+
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>' +
|
|
2219
|
+
'<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>' +
|
|
2220
|
+
'<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>' +
|
|
2221
|
+
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' +
|
|
2222
|
+
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
|
|
2223
|
+
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
|
|
2224
|
+
notesMasterOverride +
|
|
2225
|
+
overrides +
|
|
2226
|
+
notesOverrides +
|
|
2227
|
+
'</Types>'
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
function rootRelXml() {
|
|
2231
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2232
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
2233
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>' +
|
|
2234
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>' +
|
|
2235
|
+
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' +
|
|
2236
|
+
'</Relationships>'
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function coreXml(title) {
|
|
2240
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2241
|
+
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" ' +
|
|
2242
|
+
'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" ' +
|
|
2243
|
+
'xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
|
|
2244
|
+
'<dc:title>' + escapeXml(title) + '</dc:title><dc:creator>dsh-ppt</dc:creator>' +
|
|
2245
|
+
'<cp:lastModifiedBy>dsh-ppt</cp:lastModifiedBy>' +
|
|
2246
|
+
'</cp:coreProperties>'
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function appXml(slideCount, notesCount) {
|
|
2250
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2251
|
+
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" ' +
|
|
2252
|
+
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">' +
|
|
2253
|
+
'<Application>dsh-ppt</Application><PresentationFormat>Widescreen</PresentationFormat>' +
|
|
2254
|
+
'<Slides>' + slideCount + '</Slides><Notes>' + notesCount + '</Notes><HiddenSlides>0</HiddenSlides>' +
|
|
2255
|
+
'</Properties>'
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function buildZip(entries) {
|
|
2259
|
+
const chunks = []
|
|
2260
|
+
const central = []
|
|
2261
|
+
let offset = 0
|
|
2262
|
+
|
|
2263
|
+
for (const entry of entries) {
|
|
2264
|
+
const entryOffset = offset
|
|
2265
|
+
const name = Buffer.from(entry.name, 'utf8')
|
|
2266
|
+
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(String(entry.data), 'utf8')
|
|
2267
|
+
const crc = crc32(data)
|
|
2268
|
+
const compressed = deflateRawSync(data)
|
|
2269
|
+
const method = 8
|
|
2270
|
+
const nameLength = name.length
|
|
2271
|
+
const localHeader = Buffer.alloc(30)
|
|
2272
|
+
localHeader.writeUInt32LE(0x04034b50, 0)
|
|
2273
|
+
localHeader.writeUInt16LE(20, 4)
|
|
2274
|
+
localHeader.writeUInt16LE(0x0800, 6) // UTF-8 文件名
|
|
2275
|
+
localHeader.writeUInt16LE(method, 8)
|
|
2276
|
+
localHeader.writeUInt16LE(0, 10) // DOS time
|
|
2277
|
+
localHeader.writeUInt16LE(0x21, 12) // DOS date 1980-01-01
|
|
2278
|
+
localHeader.writeUInt32LE(crc, 14)
|
|
2279
|
+
localHeader.writeUInt32LE(compressed.length, 18)
|
|
2280
|
+
localHeader.writeUInt32LE(data.length, 22)
|
|
2281
|
+
localHeader.writeUInt16LE(nameLength, 26)
|
|
2282
|
+
localHeader.writeUInt16LE(0, 28)
|
|
2283
|
+
|
|
2284
|
+
chunks.push(localHeader, name, compressed)
|
|
2285
|
+
offset += 30 + nameLength + compressed.length
|
|
2286
|
+
|
|
2287
|
+
const centralHeader = Buffer.alloc(46)
|
|
2288
|
+
centralHeader.writeUInt32LE(0x02014b50, 0)
|
|
2289
|
+
centralHeader.writeUInt16LE(20, 4)
|
|
2290
|
+
centralHeader.writeUInt16LE(20, 6)
|
|
2291
|
+
centralHeader.writeUInt16LE(0x0800, 8)
|
|
2292
|
+
centralHeader.writeUInt16LE(method, 10)
|
|
2293
|
+
centralHeader.writeUInt16LE(0, 12)
|
|
2294
|
+
centralHeader.writeUInt16LE(0x21, 14)
|
|
2295
|
+
centralHeader.writeUInt32LE(crc, 16)
|
|
2296
|
+
centralHeader.writeUInt32LE(compressed.length, 20)
|
|
2297
|
+
centralHeader.writeUInt32LE(data.length, 24)
|
|
2298
|
+
centralHeader.writeUInt16LE(nameLength, 28)
|
|
2299
|
+
centralHeader.writeUInt16LE(0, 30)
|
|
2300
|
+
centralHeader.writeUInt16LE(0, 32)
|
|
2301
|
+
centralHeader.writeUInt16LE(0, 34)
|
|
2302
|
+
centralHeader.writeUInt16LE(0, 36)
|
|
2303
|
+
centralHeader.writeUInt32LE(0, 38)
|
|
2304
|
+
centralHeader.writeUInt32LE(entryOffset, 42)
|
|
2305
|
+
central.push(centralHeader, name)
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
const centralOffset = offset
|
|
2309
|
+
const centralSize = central.reduce((sum, part) => sum + part.length, 0)
|
|
2310
|
+
const end = Buffer.alloc(22)
|
|
2311
|
+
end.writeUInt32LE(0x06054b50, 0)
|
|
2312
|
+
end.writeUInt16LE(0, 4)
|
|
2313
|
+
end.writeUInt16LE(0, 6)
|
|
2314
|
+
end.writeUInt16LE(entries.length, 8)
|
|
2315
|
+
end.writeUInt16LE(entries.length, 10)
|
|
2316
|
+
end.writeUInt32LE(centralSize, 12)
|
|
2317
|
+
end.writeUInt32LE(centralOffset, 16)
|
|
2318
|
+
end.writeUInt16LE(0, 20)
|
|
2319
|
+
|
|
2320
|
+
chunks.push(...central, end)
|
|
2321
|
+
return Buffer.concat(chunks)
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
let CRC_TABLE = null
|
|
2325
|
+
function crc32(buffer) {
|
|
2326
|
+
if (CRC_TABLE === null) {
|
|
2327
|
+
CRC_TABLE = new Int32Array(256)
|
|
2328
|
+
for (let n = 0; n < 256; n += 1) {
|
|
2329
|
+
let c = n
|
|
2330
|
+
for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1)
|
|
2331
|
+
CRC_TABLE[n] = c
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
let crc = 0xffffffff
|
|
2335
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
2336
|
+
crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8)
|
|
2337
|
+
}
|
|
2338
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
export function buildPptx(manifest, themeInput, languageInput) {
|
|
2342
|
+
const theme = themeInput?.id ? themeInput : resolveTheme(themeInput)
|
|
2343
|
+
const language = languageInput?.id ? languageInput : resolveLanguage(languageInput)
|
|
2344
|
+
const motion = manifest.motion !== false
|
|
2345
|
+
const slides = manifest.slides
|
|
2346
|
+
const noteIndexes = []
|
|
2347
|
+
slides.forEach((slide, index) => {
|
|
2348
|
+
if (typeof slide.notes === 'string' && slide.notes.trim() !== '') noteIndexes.push(index)
|
|
2349
|
+
})
|
|
2350
|
+
const hasNotesMaster = noteIndexes.length > 0
|
|
2351
|
+
const entries = [
|
|
2352
|
+
{ name: '[Content_Types].xml', data: contentTypesXml(slides.length, noteIndexes) },
|
|
2353
|
+
{ name: '_rels/.rels', data: rootRelXml() },
|
|
2354
|
+
{ name: 'docProps/app.xml', data: appXml(slides.length, noteIndexes.length) },
|
|
2355
|
+
{ name: 'docProps/core.xml', data: coreXml(manifest.title) },
|
|
2356
|
+
{ name: 'ppt/presentation.xml', data: presentationXml(slides.length, hasNotesMaster) },
|
|
2357
|
+
{ name: 'ppt/_rels/presentation.xml.rels', data: presentationRelXml(slides.length, hasNotesMaster) },
|
|
2358
|
+
{ name: 'ppt/slideMasters/slideMaster1.xml', data: slideMasterXml(theme) },
|
|
2359
|
+
{ name: 'ppt/slideMasters/_rels/slideMaster1.xml.rels', data: slideMasterRelXml() },
|
|
2360
|
+
{ name: 'ppt/slideLayouts/slideLayout1.xml', data: slideLayoutXml(theme) },
|
|
2361
|
+
{ name: 'ppt/slideLayouts/_rels/slideLayout1.xml.rels', data: slideLayoutRelXml() },
|
|
2362
|
+
{ name: 'ppt/theme/theme1.xml', data: themeXml(theme) },
|
|
2363
|
+
]
|
|
2364
|
+
if (hasNotesMaster) {
|
|
2365
|
+
entries.push({ name: 'ppt/notesMasters/notesMaster1.xml', data: notesMasterXml() })
|
|
2366
|
+
entries.push({ name: 'ppt/notesMasters/_rels/notesMaster1.xml.rels', data: notesMasterRelXml() })
|
|
2367
|
+
}
|
|
2368
|
+
slides.forEach((slide, index) => {
|
|
2369
|
+
const hasNotes = noteIndexes.includes(index)
|
|
2370
|
+
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.attr, motion) })
|
|
2371
|
+
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml(hasNotes, index + 1) })
|
|
2372
|
+
if (hasNotes) {
|
|
2373
|
+
entries.push({ name: 'ppt/notesSlides/notesSlide' + (index + 1) + '.xml', data: notesSlideXml(slide.notes, language.attr) })
|
|
2374
|
+
entries.push({ name: 'ppt/notesSlides/_rels/notesSlide' + (index + 1) + '.xml.rels', data: notesSlideRelXml(index + 1) })
|
|
2375
|
+
}
|
|
2376
|
+
})
|
|
2377
|
+
return buildZip(entries)
|
|
2378
|
+
}
|