dsh-ppt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.en.md +139 -0
- package/README.md +143 -0
- package/cordis.patch.yml +17 -0
- package/lib/config.d.ts +10 -0
- package/lib/config.js +18 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.js +200 -0
- package/lib/skill.d.ts +29 -0
- package/lib/skill.js +60 -0
- package/lib/types.d.ts +65 -0
- package/lib/types.js +2 -0
- package/package.json +71 -0
- package/skills/dsh-ppt/SKILL.md +103 -0
- package/skills/dsh-ppt/references/copywriting.md +68 -0
- package/skills/dsh-ppt/references/themes.md +55 -0
- package/skills/dsh-ppt/scripts/build-deck.mjs +126 -0
- package/skills/dsh-ppt/scripts/deck-core.mjs +1222 -0
|
@@ -0,0 +1,1222 @@
|
|
|
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 { mkdirSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { resolve as resolvePath } from 'node:path'
|
|
19
|
+
import { deflateRawSync } from 'node:zlib'
|
|
20
|
+
|
|
21
|
+
export const DECK_VERSION = '0.1.0'
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// 主题
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export const THEMES = {
|
|
28
|
+
swiss: {
|
|
29
|
+
id: 'swiss',
|
|
30
|
+
name: { zh: '瑞士脉冲', en: 'Swiss Pulse' },
|
|
31
|
+
mood: { zh: '精准、理性、数据', en: 'Precise, rational, data-driven' },
|
|
32
|
+
bestFor: { zh: 'SaaS、数据、开发者工具', en: 'SaaS, data, developer tools' },
|
|
33
|
+
dark: true,
|
|
34
|
+
palette: {
|
|
35
|
+
bg: '#10151B',
|
|
36
|
+
panel: '#161D26',
|
|
37
|
+
fg: '#F5F7FA',
|
|
38
|
+
muted: '#8E9AAA',
|
|
39
|
+
accent: '#2F6BFF',
|
|
40
|
+
accent2: '#FFB300',
|
|
41
|
+
},
|
|
42
|
+
fonts: {
|
|
43
|
+
heading: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
44
|
+
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
velvet: {
|
|
48
|
+
id: 'velvet',
|
|
49
|
+
name: { zh: '天鹅绒标准', en: 'Velvet Standard' },
|
|
50
|
+
mood: { zh: '高级、克制、可信', en: 'Premium, restrained, trustworthy' },
|
|
51
|
+
bestFor: { zh: '高管汇报、品牌、融资路演', en: 'Executive decks, brand, investor pitches' },
|
|
52
|
+
dark: true,
|
|
53
|
+
palette: {
|
|
54
|
+
bg: '#111316',
|
|
55
|
+
panel: '#1A1D22',
|
|
56
|
+
fg: '#F4EFE6',
|
|
57
|
+
muted: '#A79F91',
|
|
58
|
+
accent: '#C9A84C',
|
|
59
|
+
accent2: '#3D4A63',
|
|
60
|
+
},
|
|
61
|
+
fonts: {
|
|
62
|
+
heading: 'Georgia, "Times New Roman", "Songti SC", "SimSun", serif',
|
|
63
|
+
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
data: {
|
|
67
|
+
id: 'data',
|
|
68
|
+
name: { zh: '数据漂移', en: 'Data Drift' },
|
|
69
|
+
mood: { zh: '未来、沉浸、前沿', en: 'Futuristic, immersive, cutting-edge' },
|
|
70
|
+
bestFor: { zh: 'AI、数据、研究、技术发布', en: 'AI, data, research, tech launches' },
|
|
71
|
+
dark: true,
|
|
72
|
+
palette: {
|
|
73
|
+
bg: '#070B14',
|
|
74
|
+
panel: '#0D1424',
|
|
75
|
+
fg: '#E8F1FF',
|
|
76
|
+
muted: '#7E8BA8',
|
|
77
|
+
accent: '#7C3AED',
|
|
78
|
+
accent2: '#06B6D4',
|
|
79
|
+
},
|
|
80
|
+
fonts: {
|
|
81
|
+
heading: '"Space Grotesk", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
82
|
+
body: '"Space Grotesk", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
soft: {
|
|
86
|
+
id: 'soft',
|
|
87
|
+
name: { zh: '柔和信号', en: 'Soft Signal' },
|
|
88
|
+
mood: { zh: '温暖、亲近、人本', en: 'Warm, intimate, human' },
|
|
89
|
+
bestFor: { zh: '品牌故事、培训、个人分享', en: 'Brand stories, training, personal talks' },
|
|
90
|
+
dark: false,
|
|
91
|
+
palette: {
|
|
92
|
+
bg: '#FFF8EC',
|
|
93
|
+
panel: '#FFF2DE',
|
|
94
|
+
fg: '#3B2F2A',
|
|
95
|
+
muted: '#7E6F68',
|
|
96
|
+
accent: '#E58A2F',
|
|
97
|
+
accent2: '#8FAF8C',
|
|
98
|
+
},
|
|
99
|
+
fonts: {
|
|
100
|
+
heading: '"Avenir Next", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
101
|
+
body: '"Avenir Next", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
bold: {
|
|
105
|
+
id: 'bold',
|
|
106
|
+
name: { zh: '极繁大字', en: 'Maximalist Type' },
|
|
107
|
+
mood: { zh: '大声、动能、发布', en: 'Loud, kinetic, launch' },
|
|
108
|
+
bestFor: { zh: '产品发布、活动、品牌大事件', en: 'Product launches, events, brand moments' },
|
|
109
|
+
dark: true,
|
|
110
|
+
palette: {
|
|
111
|
+
bg: '#0D0D0D',
|
|
112
|
+
panel: '#181818',
|
|
113
|
+
fg: '#FFFFFF',
|
|
114
|
+
muted: '#B8B8B8',
|
|
115
|
+
accent: '#E63946',
|
|
116
|
+
accent2: '#FFD60A',
|
|
117
|
+
},
|
|
118
|
+
fonts: {
|
|
119
|
+
heading: 'Impact, "Arial Black", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
120
|
+
body: '"Helvetica Neue", Inter, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const THEME_IDS = Object.keys(THEMES)
|
|
126
|
+
export const DEFAULT_THEME = 'data'
|
|
127
|
+
|
|
128
|
+
export const LANGUAGES = {
|
|
129
|
+
zh: {
|
|
130
|
+
id: 'zh',
|
|
131
|
+
attr: 'zh-CN',
|
|
132
|
+
ui: {
|
|
133
|
+
slide: '第',
|
|
134
|
+
of: '/ 共',
|
|
135
|
+
theme: '主题',
|
|
136
|
+
help: '← → 翻页 · F 全屏 · G 总览 · P 打印',
|
|
137
|
+
coverKicker: '开场',
|
|
138
|
+
pointKicker: '要点',
|
|
139
|
+
statementKicker: '核心观点',
|
|
140
|
+
closingTitle: '谢谢',
|
|
141
|
+
closingSubtitle: '讨论与问答',
|
|
142
|
+
generatedBy: '由 dsh-ppt 生成',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
en: {
|
|
146
|
+
id: 'en',
|
|
147
|
+
attr: 'en-US',
|
|
148
|
+
ui: {
|
|
149
|
+
slide: 'Slide',
|
|
150
|
+
of: '/',
|
|
151
|
+
theme: 'Theme',
|
|
152
|
+
help: '← → navigate · F fullscreen · G overview · P print',
|
|
153
|
+
coverKicker: 'Opening',
|
|
154
|
+
pointKicker: 'Key point',
|
|
155
|
+
statementKicker: 'Core idea',
|
|
156
|
+
closingTitle: 'Thank You',
|
|
157
|
+
closingSubtitle: 'Discussion & Q&A',
|
|
158
|
+
generatedBy: 'Generated with dsh-ppt',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
bilingual: {
|
|
162
|
+
id: 'bilingual',
|
|
163
|
+
attr: 'zh-CN',
|
|
164
|
+
ui: {
|
|
165
|
+
slide: '第',
|
|
166
|
+
of: '/ 共',
|
|
167
|
+
theme: '主题 · Theme',
|
|
168
|
+
help: '← → 翻页 · F 全屏 · G 总览 · P 打印',
|
|
169
|
+
coverKicker: '开场 · Opening',
|
|
170
|
+
pointKicker: '要点 · Key point',
|
|
171
|
+
statementKicker: '核心观点 · Core Idea',
|
|
172
|
+
closingTitle: '谢谢 · Thank You',
|
|
173
|
+
closingSubtitle: '讨论与问答 · Q&A',
|
|
174
|
+
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function resolveTheme(input) {
|
|
180
|
+
const id = String(input ?? DEFAULT_THEME).trim().toLowerCase()
|
|
181
|
+
const theme = THEMES[id]
|
|
182
|
+
if (!theme) {
|
|
183
|
+
throw new Error('dsh-ppt:未知主题 "' + id + '",可选:' + THEME_IDS.join(' / ') + '(默认 ' + DEFAULT_THEME + ')')
|
|
184
|
+
}
|
|
185
|
+
return theme
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveLanguage(input) {
|
|
189
|
+
const id = String(input ?? 'zh').trim().toLowerCase()
|
|
190
|
+
const language = LANGUAGES[id]
|
|
191
|
+
if (!language) {
|
|
192
|
+
throw new Error('dsh-ppt:未知语言 "' + id + '",可选:zh / en / bilingual')
|
|
193
|
+
}
|
|
194
|
+
return language
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function listThemes(lang = 'zh') {
|
|
198
|
+
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
199
|
+
return THEME_IDS.map((id) => {
|
|
200
|
+
const theme = THEMES[id]
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
name: pick(theme.name),
|
|
204
|
+
mood: pick(theme.mood),
|
|
205
|
+
bestFor: pick(theme.bestFor),
|
|
206
|
+
dark: theme.dark,
|
|
207
|
+
palette: { ...theme.palette },
|
|
208
|
+
fonts: { ...theme.fonts },
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// 文本工具
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
export function clampInt(value, fallback, min, max) {
|
|
218
|
+
const n = typeof value === 'number' ? Math.trunc(value) : fallback
|
|
219
|
+
if (!Number.isFinite(n)) return fallback
|
|
220
|
+
return Math.min(max, Math.max(min, n))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function sanitizeFileName(input) {
|
|
224
|
+
const base = String(input ?? 'deck').trim().replace(/\.(html?|pptx|json)$/i, '')
|
|
225
|
+
const cleaned = base
|
|
226
|
+
.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '-')
|
|
227
|
+
.replace(/\s+/g, '-')
|
|
228
|
+
.replace(/-+/g, '-')
|
|
229
|
+
.replace(/^-+|-+$/g, '')
|
|
230
|
+
return cleaned.slice(0, 120) || 'deck'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function escapeXml(value) {
|
|
234
|
+
return String(value ?? '')
|
|
235
|
+
.replace(/&/g, '&')
|
|
236
|
+
.replace(/</g, '<')
|
|
237
|
+
.replace(/>/g, '>')
|
|
238
|
+
.replace(/"/g, '"')
|
|
239
|
+
.replace(/'/g, ''')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function escapeHtml(value) {
|
|
243
|
+
return String(value ?? '')
|
|
244
|
+
.replace(/&/g, '&')
|
|
245
|
+
.replace(/</g, '<')
|
|
246
|
+
.replace(/>/g, '>')
|
|
247
|
+
.replace(/"/g, '"')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function stripInlineMarkdown(value) {
|
|
251
|
+
return String(value ?? '')
|
|
252
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
253
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
254
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
255
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
256
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
257
|
+
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
258
|
+
.replace(/_([^_\n]+)_/g, '$1')
|
|
259
|
+
.replace(/<[^>]+>/g, '')
|
|
260
|
+
.replace(/\s+/g, ' ')
|
|
261
|
+
.trim()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function splitSentences(value) {
|
|
265
|
+
const text = String(value ?? '').trim()
|
|
266
|
+
if (text === '') return []
|
|
267
|
+
const parts = text.split(/(?<=[.!?。!?…])\s+/).map((part) => part.trim()).filter(Boolean)
|
|
268
|
+
return parts.length > 0 ? parts : [text]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function truncate(value, max) {
|
|
272
|
+
const text = String(value ?? '').trim()
|
|
273
|
+
if (text.length <= max) return text
|
|
274
|
+
return text.slice(0, max - 1).replace(/\s+\S*$/, '') + '…'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function chunk(items, size) {
|
|
278
|
+
const out = []
|
|
279
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size))
|
|
280
|
+
return out
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Markdown → deck
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
function createSection(heading = '', level = 0, coverOnly = false) {
|
|
288
|
+
return { heading, level, coverOnly, bullets: [], paragraphs: [] }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
292
|
+
const language = resolveLanguage(lang)
|
|
293
|
+
const ui = language.ui
|
|
294
|
+
const title = String(titleInput ?? '').trim()
|
|
295
|
+
let coverTitle = title
|
|
296
|
+
let coverSubtitle = ''
|
|
297
|
+
let coverSubtitleConsumed = false
|
|
298
|
+
|
|
299
|
+
let body = String(content ?? '')
|
|
300
|
+
.replace(/^\uFEFF/, '')
|
|
301
|
+
.replace(/\r\n?/g, '\n')
|
|
302
|
+
// 去除文档级 YAML frontmatter(如果有)
|
|
303
|
+
body = body.replace(/^---[ \t]*\n[\s\S]*?\n---[ \t]*\n?/, '')
|
|
304
|
+
|
|
305
|
+
const sections = []
|
|
306
|
+
let current = null
|
|
307
|
+
let firstH1Seen = false
|
|
308
|
+
|
|
309
|
+
const flush = () => {
|
|
310
|
+
if (current !== null && (current.heading !== '' || current.bullets.length > 0 || current.paragraphs.length > 0)) {
|
|
311
|
+
sections.push(current)
|
|
312
|
+
}
|
|
313
|
+
current = null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const rawLine of body.split('\n')) {
|
|
317
|
+
const line = rawLine.trim()
|
|
318
|
+
if (line === '') {
|
|
319
|
+
// 无标题分组按空行分段,保证「文档无标题」时按段落生成幻灯片
|
|
320
|
+
if (current !== null && current.heading === '') flush()
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line)
|
|
325
|
+
if (headingMatch !== null) {
|
|
326
|
+
const level = headingMatch[1].length
|
|
327
|
+
const heading = stripInlineMarkdown(headingMatch[2])
|
|
328
|
+
if (level === 1 && !firstH1Seen) {
|
|
329
|
+
firstH1Seen = true
|
|
330
|
+
if (coverTitle === '') coverTitle = heading
|
|
331
|
+
flush()
|
|
332
|
+
current = createSection(heading, level, true)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
flush()
|
|
336
|
+
current = createSection(heading, level, false)
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const bulletMatch = /^\s*(?:[-*+]|\d+[.)])\s+(.+)$/.exec(line)
|
|
341
|
+
if (bulletMatch !== null) {
|
|
342
|
+
const bullet = stripInlineMarkdown(bulletMatch[1])
|
|
343
|
+
if (bullet !== '') {
|
|
344
|
+
if (current === null) current = createSection()
|
|
345
|
+
current.bullets.push(bullet)
|
|
346
|
+
}
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const paragraph = stripInlineMarkdown(line)
|
|
351
|
+
if (paragraph === '') continue
|
|
352
|
+
if (current === null) current = createSection()
|
|
353
|
+
current.paragraphs.push(paragraph)
|
|
354
|
+
}
|
|
355
|
+
flush()
|
|
356
|
+
|
|
357
|
+
const slides = []
|
|
358
|
+
const coverSource = sections.find((section) => section.coverOnly === true)
|
|
359
|
+
if (coverSource !== null && coverSource !== undefined) {
|
|
360
|
+
const coverText = coverSource.paragraphs[0] ?? coverSource.bullets[0] ?? ''
|
|
361
|
+
if (coverSubtitle === '') {
|
|
362
|
+
coverSubtitle = truncate(coverText, 180)
|
|
363
|
+
coverSubtitleConsumed = true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let bodySections = sections.filter((section) => section.coverOnly !== true)
|
|
368
|
+
// 首个 H1 既是封面又把所有要点收在封面节里时,把封面副标题之外的剩余要点
|
|
369
|
+
// 提升为一个无标题节,走下面的要点页生成逻辑,避免丢掉内容。
|
|
370
|
+
if (bodySections.length === 0 && coverSource !== null && coverSource !== undefined) {
|
|
371
|
+
const usedParagraph = coverSource.paragraphs.length > 0
|
|
372
|
+
const extras = [
|
|
373
|
+
...(usedParagraph ? coverSource.bullets : coverSource.bullets.slice(1)),
|
|
374
|
+
...coverSource.paragraphs.slice(usedParagraph ? 1 : 0),
|
|
375
|
+
]
|
|
376
|
+
if (extras.length > 0) {
|
|
377
|
+
const synthetic = createSection()
|
|
378
|
+
synthetic.bullets = extras
|
|
379
|
+
bodySections = [synthetic]
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (bodySections.length === 0) {
|
|
384
|
+
// 一句话 / 无结构输入:封面 + 核心观点 + 结束页(仍是完整的三页演示文稿)
|
|
385
|
+
const allText = coverSource !== null && coverSource !== undefined
|
|
386
|
+
? [...coverSource.bullets, ...coverSource.paragraphs].join(' ')
|
|
387
|
+
: String(content ?? '').trim()
|
|
388
|
+
if (coverSubtitle === '') coverSubtitle = truncate(splitSentences(allText)[0] ?? '', 180)
|
|
389
|
+
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
390
|
+
if (coverSubtitle !== '') {
|
|
391
|
+
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle })
|
|
392
|
+
}
|
|
393
|
+
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
394
|
+
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const hadHeadings = bodySections.some((section) => section.heading !== '')
|
|
398
|
+
|
|
399
|
+
if (!hadHeadings) {
|
|
400
|
+
// 无标题文档:第一段摘要作封面副标题,后续段落拆成要点页
|
|
401
|
+
const ordered = bodySections.map((section) => ({
|
|
402
|
+
section,
|
|
403
|
+
sentences: [...section.bullets, ...section.paragraphs].flatMap((part) => splitSentences(part)),
|
|
404
|
+
}))
|
|
405
|
+
const firstSentence = ordered[0]?.sentences[0] ?? ''
|
|
406
|
+
if (coverSubtitle === '') coverSubtitle = truncate(firstSentence, 180)
|
|
407
|
+
const rest = ordered.flatMap((group, groupIndex) => {
|
|
408
|
+
const sentences = (groupIndex === 0 && !coverSubtitleConsumed) ? group.sentences.slice(1) : group.sentences
|
|
409
|
+
return chunk(sentences, 5)
|
|
410
|
+
})
|
|
411
|
+
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
412
|
+
if (rest.length === 0) {
|
|
413
|
+
if (coverSubtitle !== '') {
|
|
414
|
+
slides.push({ layout: 'statement', kicker: ui.statementKicker, title: coverSubtitle, subtitle: coverTitle || 'Untitled' })
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
rest.forEach((points, index) => {
|
|
418
|
+
slides.push({
|
|
419
|
+
layout: 'bullets',
|
|
420
|
+
kicker: ui.pointKicker + ' ' + (index + 1),
|
|
421
|
+
title: truncate(points[0], 40) || ui.pointKicker + ' ' + (index + 1),
|
|
422
|
+
bullets: points,
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
427
|
+
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
slides.push({ layout: 'cover', kicker: ui.coverKicker, title: coverTitle || 'Untitled', subtitle: coverSubtitle })
|
|
431
|
+
let pointIndex = 0
|
|
432
|
+
for (const section of bodySections) {
|
|
433
|
+
if (section.heading === '') {
|
|
434
|
+
const points = [...section.bullets, ...section.paragraphs]
|
|
435
|
+
.flatMap((part) => splitSentences(part))
|
|
436
|
+
.slice(0, 8)
|
|
437
|
+
if (points.length > 0) {
|
|
438
|
+
pointIndex += 1
|
|
439
|
+
slides.push({
|
|
440
|
+
layout: 'bullets',
|
|
441
|
+
kicker: ui.pointKicker + ' ' + pointIndex,
|
|
442
|
+
title: truncate(points[0], 40) || ui.pointKicker + ' ' + pointIndex,
|
|
443
|
+
bullets: points,
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
continue
|
|
447
|
+
}
|
|
448
|
+
const points = [
|
|
449
|
+
...section.bullets,
|
|
450
|
+
...section.paragraphs.flatMap((part) => splitSentences(part)),
|
|
451
|
+
].slice(0, 8)
|
|
452
|
+
if (points.length > 0) {
|
|
453
|
+
slides.push({ layout: 'bullets', kicker: section.heading, title: section.heading, bullets: points })
|
|
454
|
+
} else {
|
|
455
|
+
slides.push({ layout: 'section', kicker: section.heading, title: section.heading })
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
slides.push({ layout: 'closing', title: ui.closingTitle, subtitle: coverTitle || 'Untitled' })
|
|
459
|
+
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing'])
|
|
463
|
+
|
|
464
|
+
function normalizeSlide(raw, index) {
|
|
465
|
+
const source = (raw !== null && typeof raw === 'object') ? raw : {}
|
|
466
|
+
const layout = SLIDE_LAYOUTS.has(source.layout) ? source.layout : 'bullets'
|
|
467
|
+
const title = stripInlineMarkdown(source.title ?? '')
|
|
468
|
+
const subtitle = stripInlineMarkdown(source.subtitle ?? '')
|
|
469
|
+
const kicker = stripInlineMarkdown(source.kicker ?? '')
|
|
470
|
+
const text = stripInlineMarkdown(source.text ?? '')
|
|
471
|
+
let bullets = []
|
|
472
|
+
if (Array.isArray(source.bullets)) {
|
|
473
|
+
bullets = source.bullets.map((item) => stripInlineMarkdown(String(item))).filter(Boolean)
|
|
474
|
+
} else if (typeof source.bullets === 'string' && source.bullets.trim() !== '') {
|
|
475
|
+
bullets = splitSentences(source.bullets)
|
|
476
|
+
}
|
|
477
|
+
if (layout === 'statement' && title === '' && text !== '') {
|
|
478
|
+
return { layout, kicker, title: text, subtitle: subtitle || '', bullets }
|
|
479
|
+
}
|
|
480
|
+
return { layout, kicker, title, subtitle, text, bullets }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function normalizeSlides(rawSlides, maxSlides = 60) {
|
|
484
|
+
if (!Array.isArray(rawSlides) || rawSlides.length === 0) {
|
|
485
|
+
throw new Error('dsh-ppt:slides 必须是非空数组(每个元素是 { layout, title, subtitle, kicker, bullets } 对象)')
|
|
486
|
+
}
|
|
487
|
+
const limit = clampInt(maxSlides, 60, 1, 120)
|
|
488
|
+
const bounded = rawSlides.length > limit && limit >= 3
|
|
489
|
+
? [rawSlides[0], ...rawSlides.slice(1, limit - 1), rawSlides[rawSlides.length - 1]]
|
|
490
|
+
: rawSlides.slice(0, limit)
|
|
491
|
+
const slides = bounded.map(normalizeSlide)
|
|
492
|
+
if (slides.length === 0) throw new Error('dsh-ppt:slides 规范化后为空')
|
|
493
|
+
return slides
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// 构建入口
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
export function normalizeBuildOptions(options = {}) {
|
|
501
|
+
const title = String(options.title ?? '').trim()
|
|
502
|
+
if (title === '') throw new Error('dsh-ppt:title 不能为空')
|
|
503
|
+
const theme = resolveTheme(options.theme)
|
|
504
|
+
const language = resolveLanguage(options.lang)
|
|
505
|
+
const maxSlides = clampInt(options.maxSlides, 60, 3, 120)
|
|
506
|
+
let deck
|
|
507
|
+
if (Array.isArray(options.slides) && options.slides.length > 0) {
|
|
508
|
+
deck = {
|
|
509
|
+
title,
|
|
510
|
+
subtitle: stripInlineMarkdown(options.subtitle ?? ''),
|
|
511
|
+
slides: normalizeSlides(options.slides, maxSlides),
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
const content = String(options.content ?? '').trim()
|
|
515
|
+
if (content === '') throw new Error('dsh-ppt:content 不能为空(或用 slides 传结构化幻灯片)')
|
|
516
|
+
deck = parseMarkdownDeck(title, content, language.id)
|
|
517
|
+
const slideLimit = clampInt(maxSlides, 60, 3, 120)
|
|
518
|
+
if (deck.slides.length > slideLimit) {
|
|
519
|
+
deck.slides = [deck.slides[0], ...deck.slides.slice(1, slideLimit - 1), deck.slides[deck.slides.length - 1]]
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (deck.slides.length < 1) throw new Error('dsh-ppt:没有可生成的幻灯片')
|
|
523
|
+
const outputDir = resolvePath(String(options.outputDir ?? '.').trim() || '.')
|
|
524
|
+
const fileName = sanitizeFileName(options.fileName ?? title)
|
|
525
|
+
return { title, theme, language, deck, outputDir, fileName }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function buildDeck(options = {}) {
|
|
529
|
+
const normalized = normalizeBuildOptions(options)
|
|
530
|
+
const { title, theme, language, deck, outputDir, fileName } = normalized
|
|
531
|
+
mkdirSync(outputDir, { recursive: true })
|
|
532
|
+
|
|
533
|
+
const manifest = {
|
|
534
|
+
version: DECK_VERSION,
|
|
535
|
+
title,
|
|
536
|
+
theme: theme.id,
|
|
537
|
+
language: language.id,
|
|
538
|
+
slideCount: deck.slides.length,
|
|
539
|
+
slides: deck.slides,
|
|
540
|
+
}
|
|
541
|
+
const jsonPath = resolvePath(outputDir, fileName + '.json')
|
|
542
|
+
const htmlPath = resolvePath(outputDir, fileName + '.html')
|
|
543
|
+
const pptxPath = resolvePath(outputDir, fileName + '.pptx')
|
|
544
|
+
|
|
545
|
+
writeFileSync(jsonPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
546
|
+
writeFileSync(htmlPath, renderHtml(manifest, theme, language), 'utf8')
|
|
547
|
+
writeFileSync(pptxPath, buildPptx(manifest, theme, language))
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
ok: true,
|
|
551
|
+
title,
|
|
552
|
+
theme: theme.id,
|
|
553
|
+
language: language.id,
|
|
554
|
+
slideCount: deck.slides.length,
|
|
555
|
+
outputDir,
|
|
556
|
+
files: {
|
|
557
|
+
html: htmlPath,
|
|
558
|
+
pptx: pptxPath,
|
|
559
|
+
json: jsonPath,
|
|
560
|
+
},
|
|
561
|
+
htmlPath,
|
|
562
|
+
pptxPath,
|
|
563
|
+
jsonPath,
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
// HTML 网页放映
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
export function renderHtml(manifest, theme, language) {
|
|
572
|
+
const t = theme
|
|
573
|
+
const lang = language
|
|
574
|
+
const ui = lang.ui
|
|
575
|
+
const slides = manifest.slides.map((slide, index) => renderHtmlSlide(slide, index, ui, lang.id)).join('\n')
|
|
576
|
+
const themeLabel = t.name[lang.id] ?? t.name.en
|
|
577
|
+
const total = manifest.slides.length
|
|
578
|
+
|
|
579
|
+
return `<!DOCTYPE html>
|
|
580
|
+
<html lang="${lang.attr}">
|
|
581
|
+
<head>
|
|
582
|
+
<meta charset="UTF-8">
|
|
583
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
584
|
+
<title>${escapeHtml(manifest.title)}</title>
|
|
585
|
+
<style>
|
|
586
|
+
:root{
|
|
587
|
+
--bg:${t.palette.bg};
|
|
588
|
+
--panel:${t.palette.panel};
|
|
589
|
+
--fg:${t.palette.fg};
|
|
590
|
+
--muted:${t.palette.muted};
|
|
591
|
+
--accent:${t.palette.accent};
|
|
592
|
+
--accent2:${t.palette.accent2};
|
|
593
|
+
--font-heading:${t.fonts.heading};
|
|
594
|
+
--font-body:${t.fonts.body};
|
|
595
|
+
}
|
|
596
|
+
*{box-sizing:border-box}
|
|
597
|
+
html,body{height:100%}
|
|
598
|
+
body{
|
|
599
|
+
margin:0;background:var(--bg);color:var(--fg);
|
|
600
|
+
font-family:var(--font-body);overflow:hidden;
|
|
601
|
+
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
|
|
602
|
+
}
|
|
603
|
+
.slide{
|
|
604
|
+
position:fixed;inset:0;display:none;flex-direction:column;justify-content:center;
|
|
605
|
+
padding:clamp(34px,7vw,110px);overflow:hidden;
|
|
606
|
+
}
|
|
607
|
+
.slide::before{
|
|
608
|
+
content:"";position:absolute;inset:-20%;pointer-events:none;z-index:-1;
|
|
609
|
+
background:
|
|
610
|
+
radial-gradient(42% 34% at 82% 18%, ${hexToRgba(t.palette.accent, t.dark ? 0.22 : 0.12)}, transparent 70%),
|
|
611
|
+
radial-gradient(36% 30% at 12% 86%, ${hexToRgba(t.palette.accent2, t.dark ? 0.16 : 0.12)}, transparent 70%),
|
|
612
|
+
radial-gradient(70% 60% at 50% 50%, ${hexToRgba(t.palette.panel, 0.55)}, transparent 100%);
|
|
613
|
+
}
|
|
614
|
+
.slide.is-active{display:flex;animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
615
|
+
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
616
|
+
.kicker{
|
|
617
|
+
color:var(--accent);font-weight:700;letter-spacing:.18em;text-transform:uppercase;
|
|
618
|
+
font-size:clamp(12px,1.3vw,18px);margin-bottom:22px;
|
|
619
|
+
}
|
|
620
|
+
h1,h2,.statement-title{font-family:var(--font-heading);line-height:1.06;letter-spacing:-.015em;margin:0}
|
|
621
|
+
h1{font-size:clamp(44px,7.4vw,118px);max-width:20ch}
|
|
622
|
+
h2{font-size:clamp(34px,5vw,82px);max-width:20ch}
|
|
623
|
+
.subtitle{
|
|
624
|
+
color:var(--muted);font-size:clamp(18px,2.3vw,34px);line-height:1.5;
|
|
625
|
+
max-width:46em;margin-top:28px;
|
|
626
|
+
}
|
|
627
|
+
.meta{
|
|
628
|
+
color:var(--muted);font-size:clamp(12px,1.2vw,16px);margin-top:48px;
|
|
629
|
+
letter-spacing:.06em;
|
|
630
|
+
}
|
|
631
|
+
.bullets ul{margin:30px 0 0;padding:0;list-style:none;display:grid;gap:clamp(12px,1.6vw,24px)}
|
|
632
|
+
.bullets li{
|
|
633
|
+
position:relative;padding-left:clamp(28px,2.6vw,44px);
|
|
634
|
+
font-size:clamp(20px,2.6vw,40px);line-height:1.35;max-width:24em;
|
|
635
|
+
}
|
|
636
|
+
.bullets li::before{
|
|
637
|
+
content:"";position:absolute;left:0;top:.58em;width:.5em;height:.5em;
|
|
638
|
+
background:var(--accent);border-radius:2px;box-shadow:.28em .28em 0 color-mix(in srgb, var(--accent2) 78%, transparent);
|
|
639
|
+
}
|
|
640
|
+
.section .kicker{margin-bottom:10px}
|
|
641
|
+
.section .accent-line{width:min(180px,18vw);height:6px;background:var(--accent);margin:28px 0}
|
|
642
|
+
.statement-title{
|
|
643
|
+
font-size:clamp(34px,5.4vw,88px);max-width:22ch;font-weight:800;
|
|
644
|
+
border-left:6px solid var(--accent);padding-left:clamp(22px,3vw,48px);
|
|
645
|
+
}
|
|
646
|
+
.closing{text-align:center;align-items:center}
|
|
647
|
+
.closing h1,.closing .statement-title{font-size:clamp(52px,9vw,148px)}
|
|
648
|
+
.closing .subtitle{color:var(--accent);font-weight:700}
|
|
649
|
+
.cover h1{font-weight:900}
|
|
650
|
+
#progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);z-index:30;transition:width .25s}
|
|
651
|
+
#hud{
|
|
652
|
+
position:fixed;right:22px;bottom:18px;z-index:30;display:flex;gap:14px;align-items:center;
|
|
653
|
+
color:var(--muted);font-size:13px;letter-spacing:.08em;font-variant-numeric:tabular-nums;
|
|
654
|
+
}
|
|
655
|
+
#hud button{
|
|
656
|
+
background:color-mix(in srgb, var(--panel) 88%, transparent);color:var(--fg);
|
|
657
|
+
border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;
|
|
658
|
+
padding:7px 13px;font:inherit;cursor:pointer;
|
|
659
|
+
}
|
|
660
|
+
#hud button:hover{border-color:var(--accent);color:var(--accent)}
|
|
661
|
+
body.overview .slide{display:flex !important;position:relative;inset:auto;width:100%;height:100vh}
|
|
662
|
+
body.overview{overflow:auto}
|
|
663
|
+
body.overview #progress,body.overview #hud{position:fixed}
|
|
664
|
+
@media (max-width:640px){
|
|
665
|
+
#hud{right:12px;bottom:10px;gap:8px;font-size:11px}
|
|
666
|
+
}
|
|
667
|
+
@media print{
|
|
668
|
+
html,body{height:auto;overflow:visible;background:#fff}
|
|
669
|
+
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
670
|
+
#progress,#hud{display:none !important}
|
|
671
|
+
}
|
|
672
|
+
</style>
|
|
673
|
+
</head>
|
|
674
|
+
<body>
|
|
675
|
+
<div id="progress" aria-hidden="true"></div>
|
|
676
|
+
${slides}
|
|
677
|
+
<div id="hud" aria-live="polite">
|
|
678
|
+
<span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
|
|
679
|
+
<span id="theme-label">${ui.theme} · ${escapeHtml(themeLabel)}</span>
|
|
680
|
+
<button id="fullscreen" title="F">⛶</button>
|
|
681
|
+
</div>
|
|
682
|
+
<script>
|
|
683
|
+
(() => {
|
|
684
|
+
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
685
|
+
const counter = document.getElementById('counter');
|
|
686
|
+
const progress = document.getElementById('progress');
|
|
687
|
+
const fullscreenBtn = document.getElementById('fullscreen');
|
|
688
|
+
let index = 0;
|
|
689
|
+
const total = slides.length;
|
|
690
|
+
const ui = ${JSON.stringify(ui).replace(/</g, '\\u003c')};
|
|
691
|
+
|
|
692
|
+
function go(next) {
|
|
693
|
+
index = (next + total) % total;
|
|
694
|
+
slides.forEach((slide, i) => slide.classList.toggle('is-active', i === index));
|
|
695
|
+
counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
|
|
696
|
+
progress.style.width = ((index + 1) / total * 100) + '%';
|
|
697
|
+
document.title = (index + 1) + ' / ' + total + ' · ' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
698
|
+
try { history.replaceState?.(null, '', '#slide-' + (index + 1)); } catch { /* file:// 下个别浏览器可能拒绝 */ }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
document.addEventListener('keydown', (event) => {
|
|
702
|
+
if (event.key === 'ArrowRight' || event.key === 'PageDown' || event.key === ' ') {
|
|
703
|
+
event.preventDefault(); go(index + 1);
|
|
704
|
+
} else if (event.key === 'ArrowLeft' || event.key === 'PageUp') {
|
|
705
|
+
event.preventDefault(); go(index - 1);
|
|
706
|
+
} else if (event.key === 'Home') {
|
|
707
|
+
event.preventDefault(); go(0);
|
|
708
|
+
} else if (event.key === 'End') {
|
|
709
|
+
event.preventDefault(); go(total - 1);
|
|
710
|
+
} else if (event.key.toLowerCase() === 'f') {
|
|
711
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
|
|
712
|
+
else document.exitFullscreen?.();
|
|
713
|
+
} else if (event.key.toLowerCase() === 'g') {
|
|
714
|
+
document.body.classList.toggle('overview');
|
|
715
|
+
go(index);
|
|
716
|
+
} else if (event.key.toLowerCase() === 'p') {
|
|
717
|
+
window.print();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
let wheelLock = 0;
|
|
722
|
+
document.addEventListener('wheel', (event) => {
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
if (now - wheelLock < 550 || document.body.classList.contains('overview')) return;
|
|
725
|
+
wheelLock = now;
|
|
726
|
+
if (Math.abs(event.deltaY) > 12) go(index + (event.deltaY > 0 ? 1 : -1));
|
|
727
|
+
}, { passive: true });
|
|
728
|
+
|
|
729
|
+
let touchStartY = 0;
|
|
730
|
+
document.addEventListener('touchstart', (event) => { touchStartY = event.touches[0].clientY; }, { passive: true });
|
|
731
|
+
document.addEventListener('touchend', (event) => {
|
|
732
|
+
const delta = event.changedTouches[0].clientY - touchStartY;
|
|
733
|
+
if (Math.abs(delta) > 48) go(index + (delta < 0 ? 1 : -1));
|
|
734
|
+
}, { passive: true });
|
|
735
|
+
|
|
736
|
+
fullscreenBtn.addEventListener('click', () => {
|
|
737
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
|
|
738
|
+
else document.exitFullscreen?.();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
|
|
742
|
+
go(Number.isInteger(start) ? start - 1 : 0);
|
|
743
|
+
})();
|
|
744
|
+
</script>
|
|
745
|
+
</body>
|
|
746
|
+
</html>`
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderHtmlSlide(slide, index, ui, langId) {
|
|
750
|
+
const kicker = slide.kicker || (slide.layout === 'cover' ? ui.coverKicker : '')
|
|
751
|
+
const title = slide.title || ''
|
|
752
|
+
const subtitle = slide.subtitle || ''
|
|
753
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
754
|
+
const number = String(index + 1).padStart(2, '0')
|
|
755
|
+
let inner = ''
|
|
756
|
+
switch (slide.layout) {
|
|
757
|
+
case 'cover':
|
|
758
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
759
|
+
'<h1>' + escapeHtml(title) + '</h1>' +
|
|
760
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
761
|
+
'<div class="meta">' + escapeHtml(ui.generatedBy) + '</div>'
|
|
762
|
+
break
|
|
763
|
+
case 'section':
|
|
764
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
765
|
+
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
766
|
+
'<div class="accent-line"></div>' +
|
|
767
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
768
|
+
break
|
|
769
|
+
case 'statement':
|
|
770
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
771
|
+
'<div class="statement-title">' + escapeHtml(title || slide.text || '') + '</div>' +
|
|
772
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '')
|
|
773
|
+
break
|
|
774
|
+
case 'closing':
|
|
775
|
+
inner = '<h1>' + escapeHtml(title || ui.closingTitle) + '</h1>' +
|
|
776
|
+
(subtitle !== '' ? '<div class="subtitle">' + escapeHtml(subtitle) + '</div>' : '') +
|
|
777
|
+
'<div class="meta">' + escapeHtml(ui.generatedBy) + '</div>'
|
|
778
|
+
break
|
|
779
|
+
case 'bullets':
|
|
780
|
+
default:
|
|
781
|
+
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
782
|
+
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
783
|
+
'<ul>' + bullets.map((bullet) => '<li>' + escapeHtml(bullet) + '</li>').join('') + '</ul>'
|
|
784
|
+
break
|
|
785
|
+
}
|
|
786
|
+
return '<section class="slide slide--' + escapeHtml(slide.layout || 'bullets') + '" data-index="' + number + '">' +
|
|
787
|
+
'<div class="slide-inner">' + inner + '</div></section>'
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function hexToRgba(hex, alpha) {
|
|
791
|
+
const value = String(hex).replace('#', '')
|
|
792
|
+
const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value
|
|
793
|
+
const num = Number.parseInt(full, 16)
|
|
794
|
+
const r = (num >> 16) & 255
|
|
795
|
+
const g = (num >> 8) & 255
|
|
796
|
+
const b = num & 255
|
|
797
|
+
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ---------------------------------------------------------------------------
|
|
801
|
+
// PPTX(OOXML 手写 + node:zlib zip)
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
|
|
804
|
+
const EMU_W = 12192000
|
|
805
|
+
const EMU_H = 6858000
|
|
806
|
+
|
|
807
|
+
function hex(value) {
|
|
808
|
+
return String(value).replace('#', '').toUpperCase()
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function pptxFontName(value) {
|
|
812
|
+
const match = /"?([^",]+)"?/.exec(String(value ?? ''))
|
|
813
|
+
return match?.[1]?.trim() || 'Arial'
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function paragraphXml(text, options = {}) {
|
|
817
|
+
const {
|
|
818
|
+
size = 2000,
|
|
819
|
+
color = 'FFFFFF',
|
|
820
|
+
bold = false,
|
|
821
|
+
align = 'l',
|
|
822
|
+
bullet = false,
|
|
823
|
+
font = 'Arial',
|
|
824
|
+
spaceBefore = 0,
|
|
825
|
+
spaceAfter = 0,
|
|
826
|
+
} = options
|
|
827
|
+
const runFont = pptxFontName(font)
|
|
828
|
+
let pPr = ''
|
|
829
|
+
if (bullet) {
|
|
830
|
+
pPr = '<a:pPr marL="285750" indent="-285750"><a:buFont typeface="Arial" panose="020B0604020202020204"/><a:buChar char="•"/></a:pPr>'
|
|
831
|
+
} else {
|
|
832
|
+
const parts = [] // align 与 spacing 二选一
|
|
833
|
+
if (align !== 'l') parts.push('algn="' + align + '"')
|
|
834
|
+
if (spaceBefore > 0) parts.push('<a:spcBef><a:spcPts val="' + (spaceBefore / 100) + '"/></a:spcBef>')
|
|
835
|
+
if (spaceAfter > 0) parts.push('<a:spcAft><a:spcPts val="' + (spaceAfter / 100) + '"/></a:spcAft>')
|
|
836
|
+
pPr = '<a:pPr' + (parts.length > 0 ? ' ' + parts.join(' ') : '') + '><a:buNone/></a:pPr>'
|
|
837
|
+
}
|
|
838
|
+
return '<a:p>' + pPr +
|
|
839
|
+
'<a:r><a:rPr lang="zh-CN" sz="' + size + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
840
|
+
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
841
|
+
'<a:latin typeface="' + escapeXml(runFont) + '"/><a:ea typeface="' + escapeXml(runFont) + '"/></a:rPr>' +
|
|
842
|
+
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p>'
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function textShapeXml(id, name, box, paragraphs, options = {}) {
|
|
846
|
+
const { x = 0, y = 0, w = EMU_W, h = EMU_H } = box
|
|
847
|
+
return '<p:sp>' +
|
|
848
|
+
'<p:nvSpPr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>' +
|
|
849
|
+
'<p:spPr><a:xfrm><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + w + '" cy="' + h + '"/></a:xfrm>' +
|
|
850
|
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln/></p:spPr>' +
|
|
851
|
+
'<p:txBody><a:bodyPr wrap="square" rtlCol="0"><a:normAutofit/></a:bodyPr><a:lstStyle/>' +
|
|
852
|
+
paragraphs.join('') + '</p:txBody></p:sp>'
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function accentBarXml(id, name, box, color) {
|
|
856
|
+
const { x = 0, y = 0, w = EMU_W, h = EMU_H } = box
|
|
857
|
+
return '<p:sp>' +
|
|
858
|
+
'<p:nvSpPr><p:cNvPr id="' + id + '" name="' + escapeXml(name) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>' +
|
|
859
|
+
'<p:spPr><a:xfrm><a:off x="' + x + '" y="' + y + '"/><a:ext cx="' + w + '" cy="' + h + '"/></a:xfrm>' +
|
|
860
|
+
'<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>' +
|
|
861
|
+
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill><a:ln/></p:spPr>' +
|
|
862
|
+
'<p:txBody><a:bodyPr/><a:lstStyle/><a:p/></p:txBody></p:sp>'
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function slideShapeList(slide, index, total, theme, langId) {
|
|
866
|
+
const p = theme.palette
|
|
867
|
+
const font = pptxFontName(theme.fonts.heading)
|
|
868
|
+
const bodyFont = pptxFontName(theme.fonts.body) // 保留:v0.1 后续用于统一正文字体
|
|
869
|
+
const shapes = []
|
|
870
|
+
const kicker = slide.kicker || ''
|
|
871
|
+
const title = slide.title || ''
|
|
872
|
+
const subtitle = slide.subtitle || ''
|
|
873
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
874
|
+
const idBase = (index + 1) * 10
|
|
875
|
+
const footer = String(index + 1).padStart(2, '0') + ' / ' + String(total).padStart(2, '0')
|
|
876
|
+
|
|
877
|
+
if (slide.layout === 'cover') {
|
|
878
|
+
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 2250000, w: 240000, h: 1600000 }, p.accent))
|
|
879
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 1550000, y: 2160000, w: 9250000, h: 1800000 }, [
|
|
880
|
+
paragraphXml(title, { size: 4400, color: p.fg, bold: true, font }),
|
|
881
|
+
]))
|
|
882
|
+
if (subtitle !== '') {
|
|
883
|
+
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1570000, y: 4150000, w: 9000000, h: 1200000 }, [
|
|
884
|
+
paragraphXml(subtitle, { size: 2200, color: p.muted, font: theme.fonts.body }),
|
|
885
|
+
]))
|
|
886
|
+
}
|
|
887
|
+
if (kicker !== '') {
|
|
888
|
+
shapes.push(textShapeXml(idBase + 4, 'Kicker', { x: 1570000, y: 5800000, w: 7000000, h: 500000 }, [
|
|
889
|
+
paragraphXml(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
890
|
+
]))
|
|
891
|
+
}
|
|
892
|
+
} else if (slide.layout === 'section') {
|
|
893
|
+
if (kicker !== '') {
|
|
894
|
+
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 1050000, y: 2250000, w: 9000000, h: 500000 }, [
|
|
895
|
+
paragraphXml(kicker, { size: 1600, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
896
|
+
]))
|
|
897
|
+
}
|
|
898
|
+
shapes.push(accentBarXml(idBase + 2, 'Accent bar', { x: 1050000, y: 2850000, w: 1600000, h: 160000 }, p.accent2))
|
|
899
|
+
shapes.push(textShapeXml(idBase + 3, 'Title', { x: 1050000, y: 3150000, w: 10200000, h: 1400000 }, [
|
|
900
|
+
paragraphXml(title, { size: 4000, color: p.fg, bold: true, font }),
|
|
901
|
+
]))
|
|
902
|
+
if (subtitle !== '') {
|
|
903
|
+
shapes.push(textShapeXml(idBase + 4, 'Subtitle', { x: 1070000, y: 4750000, w: 9000000, h: 900000 }, [
|
|
904
|
+
paragraphXml(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
905
|
+
]))
|
|
906
|
+
}
|
|
907
|
+
} else if (slide.layout === 'statement') {
|
|
908
|
+
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 1900000, w: 200000, h: 2800000 }, p.accent))
|
|
909
|
+
shapes.push(textShapeXml(idBase + 2, 'Statement', { x: 1500000, y: 1950000, w: 9400000, h: 2700000 }, [
|
|
910
|
+
paragraphXml(title || slide.text || '', { size: 3600, color: p.fg, bold: true, font }),
|
|
911
|
+
]))
|
|
912
|
+
if (subtitle !== '') {
|
|
913
|
+
shapes.push(textShapeXml(idBase + 3, 'Subtitle', { x: 1520000, y: 4900000, w: 9000000, h: 800000 }, [
|
|
914
|
+
paragraphXml(subtitle, { size: 1800, color: p.muted, font: theme.fonts.body }),
|
|
915
|
+
]))
|
|
916
|
+
}
|
|
917
|
+
} else if (slide.layout === 'closing') {
|
|
918
|
+
shapes.push(textShapeXml(idBase + 1, 'Title', { x: 1050000, y: 2300000, w: 10200000, h: 1700000 }, [
|
|
919
|
+
paragraphXml(title || '谢谢', { size: 5200, color: p.fg, bold: true, align: 'ctr', font }),
|
|
920
|
+
]))
|
|
921
|
+
if (subtitle !== '') {
|
|
922
|
+
shapes.push(textShapeXml(idBase + 2, 'Subtitle', { x: 1050000, y: 4200000, w: 10200000, h: 900000 }, [
|
|
923
|
+
paragraphXml(subtitle, { size: 2000, color: p.accent, bold: true, align: 'ctr', font: theme.fonts.body }),
|
|
924
|
+
]))
|
|
925
|
+
}
|
|
926
|
+
} else {
|
|
927
|
+
// bullets(默认布局)
|
|
928
|
+
if (kicker !== '') {
|
|
929
|
+
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
930
|
+
paragraphXml(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
931
|
+
]))
|
|
932
|
+
}
|
|
933
|
+
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 900000, y: 920000, w: 10300000, h: 800000 }, [
|
|
934
|
+
paragraphXml(title, { size: 3400, color: p.fg, bold: true, font }),
|
|
935
|
+
]))
|
|
936
|
+
const bodyParagraphs = bullets.length > 0
|
|
937
|
+
? bullets.map((bullet) => paragraphXml(bullet, { size: 2000, color: p.fg, bullet: true, font: theme.fonts.body, spaceAfter: 600 }))
|
|
938
|
+
: [paragraphXml(subtitle || '', { size: 2000, color: p.fg, font: theme.fonts.body })]
|
|
939
|
+
shapes.push(textShapeXml(idBase + 3, 'Body', { x: 1050000, y: 1850000, w: 10100000, h: 4500000 }, bodyParagraphs))
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
shapes.push(textShapeXml(idBase + 9, 'Page number', { x: 10600000, y: 6250000, w: 1200000, h: 400000 }, [
|
|
943
|
+
paragraphXml(footer, { size: 1000, color: p.muted, align: 'r', font: theme.fonts.body }),
|
|
944
|
+
]))
|
|
945
|
+
|
|
946
|
+
return shapes.join('')
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function slideXml(slide, index, total, theme, langId) {
|
|
950
|
+
const p = theme.palette
|
|
951
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
952
|
+
'<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
953
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
954
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
955
|
+
'<p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="' + hex(p.bg) + '"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>' +
|
|
956
|
+
'<p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
957
|
+
'<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>' +
|
|
958
|
+
slideShapeList(slide, index, total, theme, langId) +
|
|
959
|
+
'</p:spTree></p:cSld>' +
|
|
960
|
+
'<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>' +
|
|
961
|
+
'</p:sld>'
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function slideRelXml() {
|
|
965
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
966
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
967
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
968
|
+
'</Relationships>'
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function presentationXml(slideCount) {
|
|
972
|
+
const slideIds = Array.from({ length: slideCount }, (_, i) =>
|
|
973
|
+
'<p:sldId id="' + (256 + i) + '" r:id="rId' + (i + 2) + '"/>').join('')
|
|
974
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
975
|
+
'<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
976
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
977
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
978
|
+
'<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>' +
|
|
979
|
+
'<p:sldIdLst>' + slideIds + '</p:sldIdLst>' +
|
|
980
|
+
'<p:sldSz cx="' + EMU_W + '" cy="' + EMU_H + '" type="screen16x9"/>' +
|
|
981
|
+
'<p:notesSz cx="6858000" cy="9144000"/>' +
|
|
982
|
+
'</p:presentation>'
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function presentationRelXml(slideCount) {
|
|
986
|
+
const slideRels = Array.from({ length: slideCount }, (_, i) =>
|
|
987
|
+
'<Relationship Id="rId' + (i + 2) + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + (i + 1) + '.xml"/>').join('')
|
|
988
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
989
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
990
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>' +
|
|
991
|
+
slideRels +
|
|
992
|
+
'</Relationships>'
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function slideMasterXml(theme) {
|
|
996
|
+
const p = theme.palette
|
|
997
|
+
const levels = Array.from({ length: 9 }, (_, i) => {
|
|
998
|
+
const sz = Math.max(1100, 2000 - i * 100)
|
|
999
|
+
return '<a:lvl' + (i + 1) + 'pPr marL="' + (342900 + i * 342900) + '" indent="' + (-342900 - i * 0) + '">' +
|
|
1000
|
+
'<a:defRPr sz="' + sz + '"><a:solidFill><a:srgbClr val="' + hex(p.fg) + '"/></a:solidFill><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/></a:defRPr></a:lvl' + (i + 1) + 'pPr>'
|
|
1001
|
+
}).join('')
|
|
1002
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1003
|
+
'<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
1004
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
1005
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">' +
|
|
1006
|
+
'<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
1007
|
+
'<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>' +
|
|
1008
|
+
'</p:spTree></p:cSld>' +
|
|
1009
|
+
'<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"/>' +
|
|
1010
|
+
'<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>' +
|
|
1011
|
+
'<p:txStyles><p:titleStyle>' + levels + '</p:titleStyle><p:bodyStyle>' + levels + '</p:bodyStyle><p:otherStyle>' + levels + '</p:otherStyle></p:txStyles>' +
|
|
1012
|
+
'</p:sldMaster>'
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function slideMasterRelXml() {
|
|
1016
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1017
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
1018
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>' +
|
|
1019
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>' +
|
|
1020
|
+
'</Relationships>'
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function slideLayoutXml(theme) {
|
|
1024
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1025
|
+
'<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
1026
|
+
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
1027
|
+
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">' +
|
|
1028
|
+
'<p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>' +
|
|
1029
|
+
'<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>' +
|
|
1030
|
+
'</p:spTree></p:cSld>' +
|
|
1031
|
+
'<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>' +
|
|
1032
|
+
'</p:sldLayout>'
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function slideLayoutRelXml() {
|
|
1036
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1037
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
1038
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>' +
|
|
1039
|
+
'</Relationships>'
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function themeXml(theme) {
|
|
1043
|
+
const p = theme.palette
|
|
1044
|
+
const color = (name, value) => '<a:' + name + '><a:srgbClr val="' + hex(value) + '"/></a:' + name + '>'
|
|
1045
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1046
|
+
'<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="dsh-ppt ' + escapeXml(theme.name.en) + '">' +
|
|
1047
|
+
'<a:themeElements>' +
|
|
1048
|
+
'<a:clrScheme name="dsh-ppt">' +
|
|
1049
|
+
color('dk1', p.fg) + color('lt1', p.bg) + color('dk2', p.muted) + color('lt2', p.panel) +
|
|
1050
|
+
color('accent1', p.accent) + color('accent2', p.accent2) + color('accent3', p.fg) +
|
|
1051
|
+
color('accent4', p.muted) + color('accent5', p.accent) + color('accent6', p.accent2) +
|
|
1052
|
+
color('hlink', p.accent) + color('folHlink', p.accent2) +
|
|
1053
|
+
'</a:clrScheme>' +
|
|
1054
|
+
'<a:fontScheme name="dsh-ppt">' +
|
|
1055
|
+
'<a:majorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.heading)) + '"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont>' +
|
|
1056
|
+
'<a:minorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont>' +
|
|
1057
|
+
'</a:fontScheme>' +
|
|
1058
|
+
'<a:fmtScheme name="dsh-ppt">' +
|
|
1059
|
+
'<a:fillStyleLst>' +
|
|
1060
|
+
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
1061
|
+
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
1062
|
+
'<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>' +
|
|
1063
|
+
'</a:fillStyleLst>' +
|
|
1064
|
+
'<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>' +
|
|
1065
|
+
'<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>' +
|
|
1066
|
+
'<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>' +
|
|
1067
|
+
'</a:fmtScheme>' +
|
|
1068
|
+
'</a:themeElements>' +
|
|
1069
|
+
'<a:objectDefaults/><a:extraClrSchemeLst/>' +
|
|
1070
|
+
'</a:theme>'
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function contentTypesXml(slideCount) {
|
|
1074
|
+
const overrides = Array.from({ length: slideCount }, (_, i) =>
|
|
1075
|
+
'<Override PartName="/ppt/slides/slide' + (i + 1) + '.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>').join('')
|
|
1076
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1077
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
1078
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
1079
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
1080
|
+
'<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>' +
|
|
1081
|
+
'<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>' +
|
|
1082
|
+
'<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>' +
|
|
1083
|
+
'<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>' +
|
|
1084
|
+
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
|
|
1085
|
+
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
|
|
1086
|
+
overrides +
|
|
1087
|
+
'</Types>'
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function rootRelXml() {
|
|
1091
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1092
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
1093
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>' +
|
|
1094
|
+
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>' +
|
|
1095
|
+
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' +
|
|
1096
|
+
'</Relationships>'
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function coreXml(title) {
|
|
1100
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1101
|
+
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" ' +
|
|
1102
|
+
'xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" ' +
|
|
1103
|
+
'xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' +
|
|
1104
|
+
'<dc:title>' + escapeXml(title) + '</dc:title><dc:creator>dsh-ppt</dc:creator>' +
|
|
1105
|
+
'<cp:lastModifiedBy>dsh-ppt</cp:lastModifiedBy>' +
|
|
1106
|
+
'</cp:coreProperties>'
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function appXml(slideCount) {
|
|
1110
|
+
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1111
|
+
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" ' +
|
|
1112
|
+
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">' +
|
|
1113
|
+
'<Application>dsh-ppt</Application><PresentationFormat>Widescreen</PresentationFormat>' +
|
|
1114
|
+
'<Slides>' + slideCount + '</Slides><Notes>0</Notes><HiddenSlides>0</HiddenSlides>' +
|
|
1115
|
+
'</Properties>'
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function buildZip(entries) {
|
|
1119
|
+
const chunks = []
|
|
1120
|
+
const central = []
|
|
1121
|
+
let offset = 0
|
|
1122
|
+
|
|
1123
|
+
for (const entry of entries) {
|
|
1124
|
+
const name = Buffer.from(entry.name, 'utf8')
|
|
1125
|
+
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(String(entry.data), 'utf8')
|
|
1126
|
+
const crc = crc32(data)
|
|
1127
|
+
const compressed = deflateRawSync(data)
|
|
1128
|
+
const method = 8
|
|
1129
|
+
const nameLength = name.length
|
|
1130
|
+
const localHeader = Buffer.alloc(30)
|
|
1131
|
+
localHeader.writeUInt32LE(0x04034b50, 0)
|
|
1132
|
+
localHeader.writeUInt16LE(20, 4)
|
|
1133
|
+
localHeader.writeUInt16LE(0x0800, 6) // UTF-8 文件名
|
|
1134
|
+
localHeader.writeUInt16LE(method, 8)
|
|
1135
|
+
localHeader.writeUInt16LE(0, 10) // DOS time
|
|
1136
|
+
localHeader.writeUInt16LE(0x21, 12) // DOS date 1980-01-01
|
|
1137
|
+
localHeader.writeUInt32LE(crc, 14)
|
|
1138
|
+
localHeader.writeUInt32LE(compressed.length, 18)
|
|
1139
|
+
localHeader.writeUInt32LE(data.length, 22)
|
|
1140
|
+
localHeader.writeUInt16LE(nameLength, 26)
|
|
1141
|
+
localHeader.writeUInt16LE(0, 28)
|
|
1142
|
+
|
|
1143
|
+
chunks.push(localHeader, name, compressed)
|
|
1144
|
+
offset += 30 + nameLength + compressed.length
|
|
1145
|
+
|
|
1146
|
+
const centralHeader = Buffer.alloc(46)
|
|
1147
|
+
centralHeader.writeUInt32LE(0x02014b50, 0)
|
|
1148
|
+
centralHeader.writeUInt16LE(20, 4)
|
|
1149
|
+
centralHeader.writeUInt16LE(20, 6)
|
|
1150
|
+
centralHeader.writeUInt16LE(0x0800, 8)
|
|
1151
|
+
centralHeader.writeUInt16LE(method, 10)
|
|
1152
|
+
centralHeader.writeUInt16LE(0, 12)
|
|
1153
|
+
centralHeader.writeUInt16LE(0x21, 14)
|
|
1154
|
+
centralHeader.writeUInt32LE(crc, 16)
|
|
1155
|
+
centralHeader.writeUInt32LE(compressed.length, 20)
|
|
1156
|
+
centralHeader.writeUInt32LE(data.length, 24)
|
|
1157
|
+
centralHeader.writeUInt16LE(nameLength, 28)
|
|
1158
|
+
centralHeader.writeUInt16LE(0, 30)
|
|
1159
|
+
centralHeader.writeUInt16LE(0, 32)
|
|
1160
|
+
centralHeader.writeUInt16LE(0, 34)
|
|
1161
|
+
centralHeader.writeUInt16LE(0, 36)
|
|
1162
|
+
centralHeader.writeUInt32LE(0, 38)
|
|
1163
|
+
centralHeader.writeUInt32LE(0, 42)
|
|
1164
|
+
central.push(centralHeader, name)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const centralOffset = offset
|
|
1168
|
+
const centralSize = central.reduce((sum, part) => sum + part.length, 0)
|
|
1169
|
+
const end = Buffer.alloc(22)
|
|
1170
|
+
end.writeUInt32LE(0x06054b50, 0)
|
|
1171
|
+
end.writeUInt16LE(0, 4)
|
|
1172
|
+
end.writeUInt16LE(0, 6)
|
|
1173
|
+
end.writeUInt16LE(entries.length, 8)
|
|
1174
|
+
end.writeUInt16LE(entries.length, 10)
|
|
1175
|
+
end.writeUInt32LE(centralSize, 12)
|
|
1176
|
+
end.writeUInt32LE(centralOffset, 16)
|
|
1177
|
+
end.writeUInt16LE(0, 20)
|
|
1178
|
+
|
|
1179
|
+
chunks.push(...central, end)
|
|
1180
|
+
return Buffer.concat(chunks)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
let CRC_TABLE = null
|
|
1184
|
+
function crc32(buffer) {
|
|
1185
|
+
if (CRC_TABLE === null) {
|
|
1186
|
+
CRC_TABLE = new Int32Array(256)
|
|
1187
|
+
for (let n = 0; n < 256; n += 1) {
|
|
1188
|
+
let c = n
|
|
1189
|
+
for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1)
|
|
1190
|
+
CRC_TABLE[n] = c
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
let crc = 0xffffffff
|
|
1194
|
+
for (let i = 0; i < buffer.length; i += 1) {
|
|
1195
|
+
crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8)
|
|
1196
|
+
}
|
|
1197
|
+
return (crc ^ 0xffffffff) >>> 0
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
export function buildPptx(manifest, themeInput, languageInput) {
|
|
1201
|
+
const theme = themeInput?.id ? themeInput : resolveTheme(themeInput)
|
|
1202
|
+
const language = languageInput?.id ? languageInput : resolveLanguage(languageInput)
|
|
1203
|
+
const slides = manifest.slides
|
|
1204
|
+
const entries = [
|
|
1205
|
+
{ name: '[Content_Types].xml', data: contentTypesXml(slides.length) },
|
|
1206
|
+
{ name: '_rels/.rels', data: rootRelXml() },
|
|
1207
|
+
{ name: 'docProps/app.xml', data: appXml(slides.length) },
|
|
1208
|
+
{ name: 'docProps/core.xml', data: coreXml(manifest.title) },
|
|
1209
|
+
{ name: 'ppt/presentation.xml', data: presentationXml(slides.length) },
|
|
1210
|
+
{ name: 'ppt/_rels/presentation.xml.rels', data: presentationRelXml(slides.length) },
|
|
1211
|
+
{ name: 'ppt/slideMasters/slideMaster1.xml', data: slideMasterXml(theme) },
|
|
1212
|
+
{ name: 'ppt/slideMasters/_rels/slideMaster1.xml.rels', data: slideMasterRelXml() },
|
|
1213
|
+
{ name: 'ppt/slideLayouts/slideLayout1.xml', data: slideLayoutXml(theme) },
|
|
1214
|
+
{ name: 'ppt/slideLayouts/_rels/slideLayout1.xml.rels', data: slideLayoutRelXml() },
|
|
1215
|
+
{ name: 'ppt/theme/theme1.xml', data: themeXml(theme) },
|
|
1216
|
+
]
|
|
1217
|
+
slides.forEach((slide, index) => {
|
|
1218
|
+
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.id) })
|
|
1219
|
+
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml() })
|
|
1220
|
+
})
|
|
1221
|
+
return buildZip(entries)
|
|
1222
|
+
}
|