dsh-ppt 0.6.0 → 0.7.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 +96 -88
- package/LICENSE +21 -21
- package/README.en.md +219 -211
- package/README.md +234 -226
- package/cordis.patch.yml +19 -19
- package/docs/FEATURE-PLAN-2026-10-01.md +50 -50
- package/docs/LIBREOFFICE-INTEGRATION-2026-10-01.md +69 -69
- package/lib/tools.js +4 -4
- package/package.json +84 -84
- package/skills/dsh-ppt/SKILL.md +74 -74
- package/skills/dsh-ppt/package.json +11 -11
- 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 +1 -1
- 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 +460 -459
- package/skills/dsh-ppt/scripts/deck-core.mjs +702 -690
- package/skills/dsh-ppt/scripts/deck-delivery.mjs +104 -0
- package/skills/dsh-ppt/scripts/deck-media.mjs +160 -160
- package/skills/dsh-ppt/scripts/deck-render.mjs +236 -236
|
@@ -16,24 +16,25 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { resolve as resolvePath, join as joinPath } from 'node:path'
|
|
19
|
-
import { deflateRawSync } from 'node:zlib'
|
|
19
|
+
import { deflateRawSync } from 'node:zlib'
|
|
20
20
|
import { MEDIA_LAYOUTS, MEDIA_CSS, normalizeMediaSlide, renderMediaHtml, mediaBoxes } from './deck-media.mjs'
|
|
21
|
+
import { renderDelivery } from './deck-delivery.mjs'
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* manifest 版本跟随发布包 package.json;把 skills/dsh-ppt 独立复制到其他
|
|
24
25
|
* harness(没有 package.json)时回退为 0.0.0,避免版本号悄悄漂移。
|
|
25
26
|
*/
|
|
26
|
-
function readDeckVersion() {
|
|
27
|
-
for (const file of ['../../../package.json', '../package.json']) {
|
|
28
|
-
try {
|
|
29
|
-
const pkg = JSON.parse(readFileSync(new URL(file, import.meta.url), 'utf8'))
|
|
30
|
-
if (!['dsh-ppt', 'dsh-ppt-skill'].includes(pkg.name)) continue
|
|
31
|
-
const version = String(pkg.version ?? '').trim()
|
|
32
|
-
if (version) return version
|
|
33
|
-
} catch { /* Copied skill may have no parent manifest. */ }
|
|
34
|
-
}
|
|
35
|
-
return '0.0.0'
|
|
36
|
-
}
|
|
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
|
+
}
|
|
37
38
|
|
|
38
39
|
export const DECK_VERSION = readDeckVersion()
|
|
39
40
|
|
|
@@ -161,21 +162,21 @@ export const LANGUAGES = {
|
|
|
161
162
|
notesToggle: '备注',
|
|
162
163
|
closingTitle: '谢谢',
|
|
163
164
|
closingSubtitle: '讨论与问答',
|
|
164
|
-
generatedBy: '由 dsh-ppt 生成',
|
|
165
|
-
overviewToggle: '总览',
|
|
166
|
-
presenterToggle: '演讲者',
|
|
167
|
-
presenterLabel: '演讲者视图 · Presenter view',
|
|
168
|
-
helpTitle: '快捷键 · Shortcuts',
|
|
169
|
-
noNotes: '(本页无备注)',
|
|
170
|
-
closeLabel: '关闭',
|
|
171
|
-
currentLabel: '当前页 · Current',
|
|
172
|
-
nextLabel: '下一页 · Next',
|
|
173
|
-
timerLabel: '计时 · Timer',
|
|
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',
|
|
174
175
|
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示,观众看不到',
|
|
175
|
-
wrapHint: '已环绕到',
|
|
176
|
-
popupBlocked: '浏览器拦截了演讲者窗口,请允许弹出窗口后按 V 重试',
|
|
177
|
-
fullscreenUnavailable: '当前窗口无法进入全屏,请在浏览器中独立打开演示文件后重试。',
|
|
178
|
-
notesCloseLabel: '关闭备注',
|
|
176
|
+
wrapHint: '已环绕到',
|
|
177
|
+
popupBlocked: '浏览器拦截了演讲者窗口,请允许弹出窗口后按 V 重试',
|
|
178
|
+
fullscreenUnavailable: '当前窗口无法进入全屏,请在浏览器中独立打开演示文件后重试。',
|
|
179
|
+
notesCloseLabel: '关闭备注',
|
|
179
180
|
overviewHint: '点击缩略图跳页,方向键切换,Enter 进入,Esc 退出'
|
|
180
181
|
},
|
|
181
182
|
},
|
|
@@ -197,21 +198,21 @@ export const LANGUAGES = {
|
|
|
197
198
|
notesToggle: 'Notes',
|
|
198
199
|
closingTitle: 'Thank You',
|
|
199
200
|
closingSubtitle: 'Discussion & Q&A',
|
|
200
|
-
generatedBy: 'Generated with dsh-ppt',
|
|
201
|
-
overviewToggle: 'Overview',
|
|
202
|
-
presenterToggle: 'Presenter',
|
|
203
|
-
presenterLabel: 'Presenter view',
|
|
204
|
-
helpTitle: 'Keyboard shortcuts',
|
|
205
|
-
noNotes: '(No notes on this slide)',
|
|
206
|
-
closeLabel: 'Close',
|
|
207
|
-
currentLabel: 'Current',
|
|
208
|
-
nextLabel: 'Next',
|
|
209
|
-
timerLabel: 'Timer',
|
|
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',
|
|
210
211
|
presenterHint: 'Notes: S shows them on top until a presenter window is open; afterwards only in the presenter window.',
|
|
211
|
-
wrapHint: 'Wrapped to',
|
|
212
|
-
popupBlocked: 'The presenter window was blocked. Allow pop-ups and press V to retry.',
|
|
213
|
-
fullscreenUnavailable: 'Fullscreen is unavailable here. Open the presentation in a browser tab and try again.',
|
|
214
|
-
notesCloseLabel: 'Close notes',
|
|
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',
|
|
215
216
|
overviewHint: 'Click a thumbnail to jump; arrows move, Enter opens, Esc exits'
|
|
216
217
|
},
|
|
217
218
|
},
|
|
@@ -233,135 +234,135 @@ export const LANGUAGES = {
|
|
|
233
234
|
notesToggle: '备注 · Notes',
|
|
234
235
|
closingTitle: '谢谢 · Thank You',
|
|
235
236
|
closingSubtitle: '讨论与问答 · Q&A',
|
|
236
|
-
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
237
|
-
overviewToggle: '总览 · Overview',
|
|
238
|
-
presenterToggle: '演讲者 · Presenter',
|
|
239
|
-
presenterLabel: '演讲者视图 · Presenter view',
|
|
240
|
-
helpTitle: '快捷键 · Shortcuts',
|
|
241
|
-
noNotes: '(本页无备注 · No notes on this slide)',
|
|
242
|
-
closeLabel: '关闭 · Close',
|
|
243
|
-
currentLabel: '当前页 · Current',
|
|
244
|
-
nextLabel: '下一页 · Next',
|
|
245
|
-
timerLabel: '计时 · Timer',
|
|
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',
|
|
246
247
|
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示 · Notes: S shows on top until a presenter window is open',
|
|
247
|
-
wrapHint: '已环绕到 · Wrapped to',
|
|
248
|
-
popupBlocked: '浏览器拦截了演讲者窗口 · Presenter window blocked; allow pop-ups and press V',
|
|
249
|
-
fullscreenUnavailable: '当前窗口无法全屏,请独立打开演示文件 · Open this presentation in a browser tab for fullscreen.',
|
|
250
|
-
notesCloseLabel: '关闭备注 · Close notes',
|
|
251
|
-
overviewHint: '点击缩略图跳页 · Click to jump, arrows move, Enter opens, Esc exits'
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
// 主题预览(ppt_themes preview=true / buildThemePreview)
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
|
|
260
|
-
function themePages(lang) {
|
|
261
|
-
return lang === 'en'
|
|
262
|
-
? { title: 'dsh-ppt theme gallery', hint: 'Pick a theme id for ppt_create', usage: 'theme', dark: 'dark', light: 'light', palette: 'Palette' }
|
|
263
|
-
: { title: 'dsh-ppt 主题预览', hint: '把 id 填给 ppt_create 的 theme 参数', usage: 'theme', dark: '深色', light: '浅色', palette: '色板' }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** 单套主题的 960x540 SVG 色板卡:可直接放进 README / npm 首屏。 */
|
|
267
|
-
export function renderThemeSvg(input, lang = 'zh') {
|
|
268
|
-
const theme = typeof input === 'string' ? resolveTheme(input) : input
|
|
269
|
-
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
270
|
-
const p = theme.palette
|
|
271
|
-
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
272
|
-
const font = theme.fonts.heading.replace(/"/g, "'")
|
|
273
|
-
const bodyFont = theme.fonts.body.replace(/"/g, "'")
|
|
274
|
-
const out = []
|
|
275
|
-
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)) + '">')
|
|
276
|
-
out.push('<rect width="960" height="540" fill="' + p.bg + '"/>')
|
|
277
|
-
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 + '"/>')
|
|
278
|
-
out.push('<rect x="36" y="40" width="560" height="360" rx="16" fill="' + p.panel + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
279
|
-
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>')
|
|
280
|
-
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>')
|
|
281
|
-
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) + '"/>')
|
|
282
|
-
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) + '"/>')
|
|
283
|
-
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) + '"/>')
|
|
284
|
-
out.push('<rect x="64" y="312" width="500" height="34" rx="6" fill="' + p.accent + '"/>')
|
|
285
|
-
out.push('<text x="78" y="334" fill="' + p.bg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700">dsh-ppt</text>')
|
|
286
|
-
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>')
|
|
287
|
-
out.push('<rect x="64" y="346" width="500" height="2" fill="' + hexToRgba(p.muted, 0.5) + '"/>')
|
|
288
|
-
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>')
|
|
289
|
-
const swatches = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
290
|
-
const labels = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
291
|
-
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>')
|
|
292
|
-
swatches.forEach((key, i) => {
|
|
293
|
-
const y = 96 + i * 62
|
|
294
|
-
out.push('<rect x="632" y="' + y + '" width="44" height="44" rx="10" fill="' + p[key] + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
295
|
-
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>')
|
|
296
|
-
out.push('<text x="692" y="' + (y + 38) + '" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13">' + escapeXml(String(p[key]).toUpperCase()) + '</text>')
|
|
297
|
-
})
|
|
298
|
-
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>')
|
|
299
|
-
out.push('</svg>')
|
|
300
|
-
return out.join('')
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** 自包含主题对比页:5 套主题并排,含色板与用法。 */
|
|
304
|
-
export function renderThemePreviewHtml(lang = 'zh') {
|
|
305
|
-
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
306
|
-
const cards = THEME_IDS.map((id) => {
|
|
307
|
-
const theme = THEMES[id]
|
|
308
|
-
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
309
|
-
const chips = Object.keys(theme.palette).map((key) =>
|
|
310
|
-
'<span class="chip"><i style="background:' + theme.palette[key] + '"></i>' + escapeHtml(String(theme.palette[key]).toUpperCase()) + '</span>').join('')
|
|
311
|
-
return '<figure class="card" id="theme-' + escapeHtml(id) + '">' +
|
|
312
|
-
renderThemeSvg(theme, lang === 'en' ? 'en' : 'zh') +
|
|
313
|
-
'<figcaption><div class="row"><strong>' + escapeHtml(id) + '</strong><span class="badge">' + escapeHtml(pick(theme.name)) + '</span></div>' +
|
|
314
|
-
'<p class="muted">' + escapeHtml(pick(theme.mood)) + ' · ' + escapeHtml(pick(theme.bestFor)) + '</p>' +
|
|
315
|
-
'<p class="chips">' + chips + '</p>' +
|
|
316
|
-
'<p class="usage">' + escapeHtml(L.usage) + ': <code>' + escapeHtml(id) + '</code></p></figcaption></figure>'
|
|
317
|
-
}).join('')
|
|
318
|
-
return '<!DOCTYPE html>' + eolSafe('<html lang="zh-CN">') +
|
|
319
|
-
'<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">' +
|
|
320
|
-
'<title>' + escapeHtml(L.title) + '</title><style>' +
|
|
321
|
-
'body{margin:0;background:#0b0f17;color:#e8f1ff;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;padding:28px}' +
|
|
322
|
-
'h1{font-size:26px;margin:0 0 6px}p.sub{color:#8b98ad;margin:0 0 24px;font-size:14px}' +
|
|
323
|
-
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));gap:22px}' +
|
|
324
|
-
'.card{margin:0;background:#111827;border:1px solid #263043;border-radius:16px;overflow:hidden}' +
|
|
325
|
-
'.card svg{display:block;width:100%;height:auto}' +
|
|
326
|
-
'figcaption{padding:14px 16px 16px}.row{display:flex;align-items:center;gap:10px;font-size:16px}' +
|
|
327
|
-
'.badge{color:#8b98ad;font-size:13px}.muted{color:#8b98ad;font-size:13px;margin:6px 0 10px;line-height:1.5}' +
|
|
328
|
-
'.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}' +
|
|
329
|
-
'.chip i{width:10px;height:10px;border-radius:3px;display:inline-block}' +
|
|
330
|
-
'.usage{margin:0;font-size:13px;color:#aab6c9}code{background:#0b0f17;border:1px solid #263043;border-radius:6px;padding:1px 6px}' +
|
|
331
|
-
'@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}' +
|
|
332
|
-
'</style></head><body><h1>' + escapeHtml(L.title) + '</h1><p class="sub">' + escapeHtml(L.hint) + ' · dsh-ppt v' + escapeHtml(DECK_VERSION) + '</p>' +
|
|
333
|
-
'<div class="grid">' + cards + '</div></body></html>'
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function eolSafe(text) { return text }
|
|
337
|
-
|
|
338
|
-
/** 写主题对比页 + 每套主题 SVG 色板卡;默认不覆盖同名文件。 */
|
|
339
|
-
export function buildThemePreview(options = {}) {
|
|
340
|
-
const langInput = typeof options.lang === 'string' && options.lang.trim() !== '' ? options.lang.trim() : 'zh'
|
|
341
|
-
const language = resolveLanguage(langInput)
|
|
342
|
-
const outputDir = resolvePath(String(options.outputDir ?? '.'))
|
|
343
|
-
mkdirSync(outputDir, { recursive: true })
|
|
344
|
-
const overwrite = options.overwrite === true
|
|
345
|
-
const pickPath = (base, ext) => {
|
|
346
|
-
const first = resolvePath(joinPath(outputDir, base + ext))
|
|
347
|
-
if (overwrite || !existsSync(first)) return first
|
|
348
|
-
for (let i = 1; i < 1000; i += 1) {
|
|
349
|
-
const candidate = resolvePath(joinPath(outputDir, base + '-' + i + ext))
|
|
350
|
-
if (!existsSync(candidate)) return candidate
|
|
351
|
-
}
|
|
352
|
-
throw new Error('dsh-ppt:找不到可用的预览文件名(同名前缀超过 999 个),请允许 overwrite。')
|
|
353
|
-
}
|
|
354
|
-
const htmlPath = pickPath('themes-preview', '.html')
|
|
355
|
-
writeFileSync(htmlPath, renderThemePreviewHtml(language.id), 'utf8')
|
|
356
|
-
const svgs = THEME_IDS.map((id) => {
|
|
357
|
-
const svgPath = pickPath('theme-' + id, '.svg')
|
|
358
|
-
writeFileSync(svgPath, renderThemeSvg(id, language.id), 'utf8')
|
|
359
|
-
return { id, path: svgPath }
|
|
360
|
-
})
|
|
361
|
-
return { ok: true, outputDir, htmlPath, svgs, themeCount: THEME_IDS.length, language: language.id }
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
|
|
365
366
|
export function resolveTheme(input) {
|
|
366
367
|
const id = String(input ?? DEFAULT_THEME).trim().toLowerCase()
|
|
367
368
|
const theme = THEMES[id]
|
|
@@ -433,7 +434,7 @@ const DECK_ARTIFACT_EXTENSIONS = ['.html', '.pptx', '.json']
|
|
|
433
434
|
* 为三件套选择同一个可用文件名前缀。默认不覆盖:任一产物已存在时,
|
|
434
435
|
* 整组改用 -1/-2… 后缀,避免新旧 deck 被静默混写。
|
|
435
436
|
*/
|
|
436
|
-
export function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
|
|
437
|
+
export function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
|
|
437
438
|
// 一次 readdir 建集,替代逐个候选 existsSync 探测(最坏 ~3000 次 stat);
|
|
438
439
|
// 统一小写比较,避免 Windows 大小写不敏感文件系统把已占用的名字判成可用。
|
|
439
440
|
const existing = new Set(readdirSync(outputDir).map((name) => name.toLowerCase()))
|
|
@@ -797,11 +798,11 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
797
798
|
return { title: coverTitle || 'Untitled', subtitle: coverSubtitle, slides }
|
|
798
799
|
}
|
|
799
800
|
|
|
800
|
-
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing', 'quote', 'table', ...MEDIA_LAYOUTS])
|
|
801
|
+
const SLIDE_LAYOUTS = new Set(['cover', 'section', 'bullets', 'statement', 'closing', 'quote', 'table', ...MEDIA_LAYOUTS])
|
|
801
802
|
|
|
802
803
|
function normalizeSlide(raw, index) {
|
|
803
804
|
const source = (raw !== null && typeof raw === 'object') ? raw : {}
|
|
804
|
-
const layout = SLIDE_LAYOUTS.has(source.layout) ? source.layout : source.chart ? 'chart' : source.image ? 'image-right' : 'bullets'
|
|
805
|
+
const layout = SLIDE_LAYOUTS.has(source.layout) ? source.layout : source.chart ? 'chart' : source.image ? 'image-right' : 'bullets'
|
|
805
806
|
const title = stripInlineMarkdown(source.title ?? '')
|
|
806
807
|
const subtitle = stripInlineMarkdown(source.subtitle ?? '')
|
|
807
808
|
const kicker = stripInlineMarkdown(source.kicker ?? '')
|
|
@@ -811,10 +812,10 @@ function normalizeSlide(raw, index) {
|
|
|
811
812
|
let bullets = []
|
|
812
813
|
if (Array.isArray(source.bullets)) {
|
|
813
814
|
bullets = source.bullets.map((item) => stripInlineMarkdown(String(item))).filter(Boolean)
|
|
814
|
-
} else if (typeof source.bullets === 'string' && source.bullets.trim() !== '') {
|
|
815
|
-
bullets = splitSentences(source.bullets)
|
|
816
|
-
}
|
|
817
|
-
if (MEDIA_LAYOUTS.has(layout)) return normalizeMediaSlide(source, { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }, index)
|
|
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)
|
|
818
819
|
if (layout === 'statement' && title === '' && text !== '') {
|
|
819
820
|
return { layout, kicker, title: text, subtitle: subtitle || '', bullets, ...(notes !== '' ? { notes } : {}) }
|
|
820
821
|
}
|
|
@@ -895,7 +896,7 @@ export function normalizeBuildOptions(options = {}) {
|
|
|
895
896
|
* 任一步失败都清理临时文件,并把已提交的目标恢复为调用前的内容,
|
|
896
897
|
* 避免出现「json/html 已更新、pptx 还是旧的」这类混版状态。
|
|
897
898
|
*/
|
|
898
|
-
export function commitDeckArtifacts(artifacts) {
|
|
899
|
+
export function commitDeckArtifacts(artifacts) {
|
|
899
900
|
const stamp = process.pid.toString(36) + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8)
|
|
900
901
|
const items = artifacts.map((item, index) => ({ ...item, tempPath: item.path + '.' + stamp + '-' + index + '.tmp' }))
|
|
901
902
|
const removeTemp = (item) => {
|
|
@@ -948,10 +949,10 @@ export function commitDeckArtifacts(artifacts) {
|
|
|
948
949
|
}
|
|
949
950
|
}
|
|
950
951
|
|
|
951
|
-
export function buildDeck(options = {}) {
|
|
952
|
-
if (options.brand || options.template || options.slides?.some(slide => MEDIA_LAYOUTS.has(slide.layout) || slide.image || slide.chart)) {
|
|
953
|
-
throw new Error('dsh-ppt:图片、图表、模板与品牌请使用异步 buildDeckAsync 或 ppt_create / CLI')
|
|
954
|
-
}
|
|
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
|
+
}
|
|
955
956
|
const normalized = normalizeBuildOptions(options)
|
|
956
957
|
const { title, theme, language, motion, deck, outputDir, overwrite } = normalized
|
|
957
958
|
mkdirSync(outputDir, { recursive: true })
|
|
@@ -973,7 +974,7 @@ export function buildDeck(options = {}) {
|
|
|
973
974
|
// 先生成三份内容再统一提交:生成阶段抛错时也不会留下半套文件。
|
|
974
975
|
commitDeckArtifacts([
|
|
975
976
|
{ path: jsonPath, data: JSON.stringify(manifest, null, 2) + '\n', encoding: 'utf8' },
|
|
976
|
-
{ path: htmlPath, data: renderHtml(manifest, theme, language), encoding: 'utf8' },
|
|
977
|
+
{ path: htmlPath, data: renderHtml(manifest, theme, language, { pptx: pptxPath, json: jsonPath }), encoding: 'utf8' },
|
|
977
978
|
{ path: pptxPath, data: buildPptx(manifest, theme, language) },
|
|
978
979
|
])
|
|
979
980
|
|
|
@@ -999,191 +1000,192 @@ export function buildDeck(options = {}) {
|
|
|
999
1000
|
// HTML 网页放映
|
|
1000
1001
|
// ---------------------------------------------------------------------------
|
|
1001
1002
|
|
|
1002
|
-
export function renderHtml(manifest, theme, language) {
|
|
1003
|
-
const t = theme
|
|
1004
|
-
const lang = language
|
|
1005
|
-
const ui = lang.ui
|
|
1006
|
-
const motion = manifest.motion !== false
|
|
1007
|
-
const motionCss = motion
|
|
1008
|
-
? `body.motion .slide.is-active{animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
1009
|
-
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
1010
|
-
body.motion .bullets li{
|
|
1011
|
-
opacity:0;animation:bullet-in .55s cubic-bezier(.22,.8,.36,1) forwards;
|
|
1012
|
-
animation-delay:calc(var(--i, 0)*90ms + .12s);
|
|
1013
|
-
}
|
|
1014
|
-
@keyframes bullet-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}`
|
|
1015
|
-
: ''
|
|
1016
|
-
const slides = manifest.slides.map((slide, index) => {
|
|
1017
|
-
const label = slide.title || slide.kicker || (ui.slide + ' ' + (index + 1))
|
|
1018
|
-
return '<div class="slide-frame" data-frame="' + (index + 1) + '" tabindex="-1" role="button" aria-label="' + escapeHtml(String(label)) + '">' +
|
|
1019
|
-
renderHtmlSlide(slide, index, ui, lang.id, manifest, theme) + '</div>'
|
|
1020
|
-
}).join('\n')
|
|
1021
|
-
const themeLabel = t.name[lang.id] ?? t.name.en
|
|
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
|
|
1022
1023
|
const total = manifest.slides.length
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
<
|
|
1027
|
-
<
|
|
1028
|
-
<meta
|
|
1029
|
-
<
|
|
1030
|
-
<
|
|
1031
|
-
<
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
--
|
|
1035
|
-
--
|
|
1036
|
-
--
|
|
1037
|
-
--
|
|
1038
|
-
--
|
|
1039
|
-
--
|
|
1040
|
-
--font-
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
body{
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
.slide{
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
radial-gradient(
|
|
1060
|
-
radial-gradient(
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
${
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
h1{font-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
.bullets
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
.section .
|
|
1091
|
-
.
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
.closing
|
|
1097
|
-
.closing .
|
|
1098
|
-
.
|
|
1099
|
-
.
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
font-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
color
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
.deck-table
|
|
1123
|
-
|
|
1124
|
-
#
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
#
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
#notes-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
#notes-
|
|
1148
|
-
|
|
1149
|
-
font-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
#notes-text.
|
|
1153
|
-
#
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
#help-panel
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
#
|
|
1172
|
-
|
|
1173
|
-
body.blank-
|
|
1174
|
-
body.blank-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
grid-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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{
|
|
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)}
|
|
1187
1189
|
body.overview .slide-frame.is-current{outline:2px solid var(--accent);outline-offset:2px}
|
|
1188
1190
|
body.overview .slide-frame:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
|
|
1189
1191
|
body.overview .slide-frame::after{
|
|
@@ -1191,126 +1193,130 @@ body.overview .slide-frame::after{
|
|
|
1191
1193
|
font-size:12px;color:var(--muted);background:color-mix(in srgb, var(--bg) 84%, transparent);
|
|
1192
1194
|
border:1px solid color-mix(in srgb, var(--muted) 40%, transparent);border-radius:6px;
|
|
1193
1195
|
padding:1px 7px;font-variant-numeric:tabular-nums;
|
|
1194
|
-
}
|
|
1195
|
-
body.overview .slide{
|
|
1196
|
-
display:flex !important;position:absolute;inset:0;width:1280px;height:720px;
|
|
1197
|
-
transform:scale(var(--deck-thumb-scale,.25));transform-origin:top left;pointer-events:none;
|
|
1198
|
-
animation:none !important;
|
|
1199
|
-
}
|
|
1200
|
-
body.overview .bullets li{opacity:1 !important;animation:none !important}
|
|
1201
|
-
body.overview .media-canvas{width:1280px;height:720px}
|
|
1202
|
-
body.overview #notes-panel,body.presenting #notes-panel{display:none !important}
|
|
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}
|
|
1203
1205
|
body.presenting #notes-toggle{opacity:.55}
|
|
1204
1206
|
body.presenting #hud{opacity:0;pointer-events:none;transition:opacity .25s}
|
|
1205
|
-
body.presenting #hud:hover{opacity:1;pointer-events:auto}
|
|
1206
|
-
@media (max-width:900px){
|
|
1207
|
-
.slide{padding:clamp(22px,5vw,44px)}
|
|
1208
|
-
h1{font-size:clamp(34px,8.5vw,64px)}
|
|
1209
|
-
h2{font-size:clamp(26px,6.5vw,48px)}
|
|
1210
|
-
.statement-title{font-size:clamp(26px,7vw,54px)}
|
|
1211
|
-
.subtitle{font-size:clamp(16px,3.4vw,24px)}
|
|
1212
|
-
.bullets li{font-size:clamp(16px,3.4vw,24px)}
|
|
1213
|
-
.kicker{font-size:13px}
|
|
1214
|
-
.deck-table{font-size:14px}
|
|
1215
|
-
.deck-table th,.deck-table td{padding:.55em .6em}
|
|
1216
|
-
#hud{right:10px;bottom:10px;gap:
|
|
1217
|
-
#hud button{padding:10px 16px;min-height:44px}
|
|
1218
|
-
#notes-panel{bottom:68px;max-height:45vh}
|
|
1219
|
-
#help-panel{right:10px;bottom:68px;left:10px;max-width:none}
|
|
1220
|
-
}
|
|
1221
|
-
@media (max-width:640px){
|
|
1222
|
-
#theme-label{display:none}
|
|
1223
|
-
}
|
|
1224
|
-
@media (prefers-reduced-motion: reduce){
|
|
1225
|
-
*,*::before,*::after{animation:none !important;transition:none !important}
|
|
1226
|
-
}
|
|
1227
|
-
@media print{
|
|
1228
|
-
html,body{height:auto;overflow:visible;background:#fff}
|
|
1229
|
-
.slide-frame{position:relative;display:block !important}
|
|
1230
|
-
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
1231
|
-
${motion ? 'body.motion .bullets li{opacity:1 !important;animation:none !important}' : ''}
|
|
1232
|
-
#progress,#hud,#notes-panel,#help-panel,#toast{display:none !important}
|
|
1233
|
-
}
|
|
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
|
+
#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
|
+
}
|
|
1236
|
+
${delivery.css}
|
|
1234
1237
|
</style>
|
|
1235
1238
|
</head>
|
|
1236
1239
|
<body class="${motion ? 'motion' : 'no-motion'}">
|
|
1237
1240
|
<div id="progress" aria-hidden="true"></div>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1241
|
+
${delivery.body}
|
|
1242
|
+
<div id="stage">
|
|
1243
|
+
${slides}
|
|
1244
|
+
</div>
|
|
1245
|
+
<div id="hud" aria-live="polite">
|
|
1242
1246
|
<span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
|
|
1243
|
-
|
|
1244
|
-
<
|
|
1245
|
-
<button id="
|
|
1246
|
-
<button id="
|
|
1247
|
-
<button id="
|
|
1248
|
-
<button id="
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
<
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
<
|
|
1260
|
-
<p
|
|
1261
|
-
<p>${escapeHtml(ui.
|
|
1262
|
-
</
|
|
1263
|
-
|
|
1264
|
-
<div id="
|
|
1265
|
-
<
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const
|
|
1269
|
-
const
|
|
1270
|
-
const
|
|
1271
|
-
const
|
|
1272
|
-
const
|
|
1273
|
-
const
|
|
1274
|
-
const
|
|
1275
|
-
const
|
|
1276
|
-
const
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
const
|
|
1280
|
-
const
|
|
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');
|
|
1281
1286
|
let index = 0;
|
|
1282
|
-
let
|
|
1283
|
-
let
|
|
1284
|
-
let
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
function
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
toastEl.
|
|
1302
|
-
|
|
1303
|
-
toastTimer
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
notesText.
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
+
|
|
1314
1320
|
function sendInit() {
|
|
1315
1321
|
if (!presenterOpen()) return;
|
|
1316
1322
|
try {
|
|
@@ -1343,54 +1349,55 @@ ${slides}
|
|
|
1343
1349
|
|
|
1344
1350
|
function updatePresenter() { sendState(); }
|
|
1345
1351
|
|
|
1346
|
-
function go(next) {
|
|
1347
|
-
if (document.body.classList.contains('blank-black') || document.body.classList.contains('blank-white')) setBlank(null);
|
|
1348
|
-
const wrapped = next < 0 || next >= total;
|
|
1349
|
-
index = (next + total) % total;
|
|
1350
|
-
slides.forEach(function (slide, i) {
|
|
1351
|
-
slide.classList.toggle('is-active', i === index);
|
|
1352
|
-
if (frames[i]) frames[i].classList.toggle('is-current', i === index);
|
|
1353
|
-
});
|
|
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
|
+
});
|
|
1354
1360
|
counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
function
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
document.documentElement.style.setProperty('--deck-thumb-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
|
|
1394
1401
|
function presenterHtml() {
|
|
1395
1402
|
const labelCurrent = ${JSON.stringify(ui.currentLabel)};
|
|
1396
1403
|
const labelNext = ${JSON.stringify(ui.nextLabel)};
|
|
@@ -1437,7 +1444,7 @@ ${slides}
|
|
|
1437
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}',
|
|
1438
1445
|
'function clock(){var d=new Date();return ("0"+d.getHours()).slice(-2)+":"+("0"+d.getMinutes()).slice(-2)+":"+("0"+d.getSeconds()).slice(-2)}',
|
|
1439
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)}',
|
|
1440
|
-
'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")}',
|
|
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")}',
|
|
1441
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])}}',
|
|
1442
1449
|
'function setSlide(box,html){box.innerHTML="";var f=document.createElement("div");f.className="pv-slide";f.innerHTML=html;box.appendChild(f)}',
|
|
1443
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()}',
|
|
@@ -1471,25 +1478,25 @@ ${slides}
|
|
|
1471
1478
|
+ '<scr' + 'ipt>' + js + '</scr' + 'ipt></body></html>';
|
|
1472
1479
|
}
|
|
1473
1480
|
|
|
1474
|
-
async function toggleFullscreen() {
|
|
1475
|
-
try {
|
|
1476
|
-
if (document.fullscreenElement) { await document.exitFullscreen?.(); return; }
|
|
1477
|
-
if (!document.documentElement.requestFullscreen || document.fullscreenEnabled === false) {
|
|
1478
|
-
toast(ui.fullscreenUnavailable);
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
await document.documentElement.requestFullscreen();
|
|
1482
|
-
if (!document.fullscreenElement) toast(ui.fullscreenUnavailable);
|
|
1483
|
-
} catch (_) { toast(ui.fullscreenUnavailable); }
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
function openPresenter() {
|
|
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() {
|
|
1487
1494
|
if (presenterOpen()) { try { presenterWin.focus(); } catch (_) { /* gone */ } return; }
|
|
1488
1495
|
let url = '';
|
|
1489
1496
|
try {
|
|
1490
1497
|
url = URL.createObjectURL(new Blob([presenterHtml()], { type: 'text/html' }));
|
|
1491
1498
|
} catch (_) { toast(ui.popupBlocked); return; }
|
|
1492
|
-
presenterInitSent = false;
|
|
1499
|
+
presenterInitSent = false;
|
|
1493
1500
|
presenterWin = window.open(url, 'dsh-ppt-presenter', 'width=1180,height=760');
|
|
1494
1501
|
if (!presenterWin) {
|
|
1495
1502
|
try { URL.revokeObjectURL(url); } catch (_) { /* ignore */ }
|
|
@@ -1531,10 +1538,10 @@ ${slides}
|
|
|
1531
1538
|
const data = event.data || {};
|
|
1532
1539
|
if (data.__dshPpt === 'cmd' && (data.kind === 'end' || data.kind === 'closed')) { closePresenter(); return; }
|
|
1533
1540
|
if (!presenterOpen()) return;
|
|
1534
|
-
if (data.__dshPpt === 'ready') {
|
|
1535
|
-
if (!presenterInitSent) { presenterInitSent = true; sendInit(); }
|
|
1536
|
-
sendState();
|
|
1537
|
-
return;
|
|
1541
|
+
if (data.__dshPpt === 'ready') {
|
|
1542
|
+
if (!presenterInitSent) { presenterInitSent = true; sendInit(); }
|
|
1543
|
+
sendState();
|
|
1544
|
+
return;
|
|
1538
1545
|
}
|
|
1539
1546
|
if (data.__dshPpt !== 'cmd') return;
|
|
1540
1547
|
if (data.kind === 'go') {
|
|
@@ -1552,111 +1559,116 @@ ${slides}
|
|
|
1552
1559
|
}
|
|
1553
1560
|
});
|
|
1554
1561
|
|
|
1555
|
-
document.addEventListener('keydown', (event) => {
|
|
1556
|
-
const key = event.key;
|
|
1557
|
-
const lower = key.toLowerCase();
|
|
1562
|
+
document.addEventListener('keydown', (event) => {
|
|
1563
|
+
const key = event.key;
|
|
1564
|
+
const lower = key.toLowerCase();
|
|
1558
1565
|
const targetEl = event.target;
|
|
1559
|
-
|
|
1560
|
-
if (key
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
if (
|
|
1565
|
-
if (document.body.classList.contains('
|
|
1566
|
-
if (
|
|
1567
|
-
return;
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
event.preventDefault(); go(index
|
|
1574
|
-
} else if (
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
event.preventDefault(); go(
|
|
1579
|
-
} else if (key === '
|
|
1580
|
-
event.preventDefault(); go(
|
|
1581
|
-
} else if (key === '
|
|
1582
|
-
event.preventDefault();
|
|
1583
|
-
} else if (
|
|
1584
|
-
|
|
1585
|
-
} else if (lower === '
|
|
1586
|
-
|
|
1587
|
-
} else if (lower === '
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
} else if (lower === '
|
|
1596
|
-
setBlank(document.body.classList.contains('blank-
|
|
1597
|
-
} else if (lower === '
|
|
1598
|
-
|
|
1599
|
-
} else if (
|
|
1600
|
-
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1566
|
+
if (document.getElementById('review-panel').open || event.ctrlKey || event.metaKey || event.altKey) return;
|
|
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;
|
|
1605
1614
|
document.addEventListener('wheel', (event) => {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
wheelLock
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
+
|
|
1612
1622
|
let touchStartY = 0;
|
|
1613
|
-
|
|
1614
|
-
document.addEventListener('
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
window.addEventListener('
|
|
1638
|
-
|
|
1623
|
+
let touchOnStage = false;
|
|
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;
|
|
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
|
+
|
|
1650
|
+
${delivery.script}
|
|
1639
1651
|
const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
|
|
1640
|
-
go(Number.isInteger(start) ? start - 1 : 0);
|
|
1641
|
-
})();
|
|
1642
|
-
</script>
|
|
1643
|
-
</body>
|
|
1644
|
-
</html>`
|
|
1645
|
-
}
|
|
1646
|
-
function renderHtmlSlide(slide, index, ui, langId, manifest = {}, theme) {
|
|
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) {
|
|
1647
1659
|
const kicker = slide.kicker || (slide.layout === 'cover' ? ui.coverKicker : '')
|
|
1648
1660
|
const title = slide.title || ''
|
|
1649
1661
|
const subtitle = slide.subtitle || ''
|
|
1650
1662
|
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
1651
1663
|
const number = String(index + 1).padStart(2, '0')
|
|
1652
1664
|
let inner = ''
|
|
1653
|
-
switch (slide.layout) {
|
|
1654
|
-
case 'image':
|
|
1655
|
-
case 'image-left':
|
|
1656
|
-
case 'image-right':
|
|
1657
|
-
case 'chart':
|
|
1658
|
-
inner = renderMediaHtml(slide, manifest, theme)
|
|
1659
|
-
break
|
|
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
|
|
1660
1672
|
case 'cover':
|
|
1661
1673
|
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
1662
1674
|
'<h1>' + escapeHtml(title) + '</h1>' +
|
|
@@ -1711,10 +1723,10 @@ function renderHtmlSlide(slide, index, ui, langId, manifest = {}, theme) {
|
|
|
1711
1723
|
const notesAttr = typeof slide.notes === 'string' && slide.notes.trim() !== ''
|
|
1712
1724
|
? ' data-notes="' + escapeHtml(slide.notes) + '"'
|
|
1713
1725
|
: ''
|
|
1714
|
-
return '<section class="slide slide--' + escapeHtml(slide.layout || 'bullets') + '" data-index="' + number + '"' + notesAttr + '>' +
|
|
1715
|
-
'<div class="slide-inner">' + inner + '</div>' +
|
|
1716
|
-
(manifest.brand?.logoAssetId ? '<img class="brand-logo" src="' + escapeHtml(manifest.assets?.[manifest.brand.logoAssetId]?.data ?? '') + '" alt="' + escapeHtml(manifest.brand.name || 'Logo') + '"/>' : '') +
|
|
1717
|
-
(manifest.brand?.footer ? '<div class="brand-footer">' + escapeHtml(manifest.brand.footer) + '</div>' : '') + '</section>'
|
|
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>'
|
|
1718
1730
|
}
|
|
1719
1731
|
|
|
1720
1732
|
function hexToRgba(hex, alpha) {
|
|
@@ -1738,29 +1750,29 @@ function hex(value) {
|
|
|
1738
1750
|
return String(value).replace('#', '').toUpperCase()
|
|
1739
1751
|
}
|
|
1740
1752
|
|
|
1741
|
-
export function pptxFontName(value) {
|
|
1742
|
-
const names = String(value ?? '').split(',').map(name => name.trim().replace(/^["']|["']$/g, '')).filter(Boolean)
|
|
1743
|
-
// A CSS stack may start with an optional web font. Office receives one actual
|
|
1744
|
-
// family, so select a platform fallback instead of copying the first name.
|
|
1745
|
-
if (names.length === 1 && !['serif', 'sans-serif', 'system-ui'].includes(names[0])) return names[0]
|
|
1746
|
-
const available = process.platform === 'win32'
|
|
1747
|
-
? ['Segoe UI', 'Arial', 'Arial Black', 'Impact', 'Georgia', 'Times New Roman', 'Microsoft YaHei', 'SimSun']
|
|
1748
|
-
: process.platform === 'darwin'
|
|
1749
|
-
? ['Helvetica Neue', 'Arial', 'Impact', 'Georgia', 'Times New Roman', 'Avenir Next', 'PingFang SC', 'Songti SC']
|
|
1750
|
-
: ['Liberation Sans', 'Liberation Serif', 'Noto Sans', 'Noto Sans CJK SC', 'Noto Serif CJK SC']
|
|
1751
|
-
return names.find(name => available.includes(name)) || (names.includes('serif')
|
|
1752
|
-
? process.platform === 'linux' ? 'Liberation Serif' : 'Georgia'
|
|
1753
|
-
: process.platform === 'win32' ? 'Segoe UI' : process.platform === 'darwin' ? 'Helvetica Neue' : 'Liberation Sans')
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
export function pptxEastAsianFont(value) {
|
|
1757
|
-
const font = pptxFontName(value)
|
|
1758
|
-
if (/Microsoft YaHei|SimSun|SimHei|PingFang|Songti|Heiti|Noto (Sans|Serif) CJK|[\u3400-\u9fff]/i.test(font)) return font
|
|
1759
|
-
const serif = /Georgia|Times New Roman|Cambria|Liberation Serif/i.test(font)
|
|
1760
|
-
return process.platform === 'win32' ? serif ? 'SimSun' : 'Microsoft YaHei'
|
|
1761
|
-
: process.platform === 'darwin' ? serif ? 'Songti SC' : 'PingFang SC'
|
|
1762
|
-
: serif ? 'Noto Serif CJK SC' : 'Noto Sans CJK SC'
|
|
1763
|
-
}
|
|
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
|
+
}
|
|
1764
1776
|
|
|
1765
1777
|
function paragraphXml(text, options = {}) {
|
|
1766
1778
|
const {
|
|
@@ -1775,14 +1787,14 @@ function paragraphXml(text, options = {}) {
|
|
|
1775
1787
|
spaceAfter = 0,
|
|
1776
1788
|
} = options
|
|
1777
1789
|
const runFont = pptxFontName(font)
|
|
1778
|
-
const spacing = (spaceBefore > 0 ? '<a:spcBef><a:spcPts val="' + spaceBefore + '"/></a:spcBef>' : '') +
|
|
1779
|
-
(spaceAfter > 0 ? '<a:spcAft><a:spcPts val="' + spaceAfter + '"/></a:spcAft>' : '')
|
|
1780
|
-
const pPr = '<a:pPr' + (bullet ? ' marL="285750" indent="-285750"' : align !== 'l' ? ' algn="' + align + '"' : '') + '>' + spacing +
|
|
1781
|
-
(bullet ? '<a:buFont typeface="Arial" panose="020B0604020202020204"/><a:buChar char="•"/>' : '<a:buNone/>') + '</a:pPr>'
|
|
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>'
|
|
1782
1794
|
return '<a:p>' + pPr +
|
|
1783
1795
|
'<a:r><a:rPr lang="' + lang + '" sz="' + size + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1784
1796
|
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1785
|
-
'<a:latin typeface="' + escapeXml(runFont) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(font)) + '"/></a:rPr>' +
|
|
1797
|
+
'<a:latin typeface="' + escapeXml(runFont) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(font)) + '"/></a:rPr>' +
|
|
1786
1798
|
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p>'
|
|
1787
1799
|
}
|
|
1788
1800
|
|
|
@@ -1812,16 +1824,16 @@ function tableGraphicXml(id, name, box, rows, theme, langId) {
|
|
|
1812
1824
|
const lang = langId || 'zh-CN'
|
|
1813
1825
|
const font = pptxFontName(theme.fonts.body)
|
|
1814
1826
|
const safeRows = rows.length > 0 ? rows : [['']]
|
|
1815
|
-
const cols = Math.max(1, safeRows[0].length)
|
|
1816
|
-
const fontSize = cols <= 6 ? 1700 : 1400
|
|
1827
|
+
const cols = Math.max(1, safeRows[0].length)
|
|
1828
|
+
const fontSize = cols <= 6 ? 1700 : 1400
|
|
1817
1829
|
const colW = Math.floor(box.w / cols)
|
|
1818
1830
|
const grid = Array.from({ length: cols }, () => '<a:gridCol w="' + colW + '"/>').join('')
|
|
1819
1831
|
const rowH = 420000
|
|
1820
1832
|
const cellXml = (text, fill, color, bold) =>
|
|
1821
1833
|
'<a:tc><a:txBody><a:bodyPr anchor="ctr"/><a:lstStyle/><a:p><a:pPr algn="l"><a:buNone/></a:pPr>' +
|
|
1822
|
-
'<a:r><a:rPr lang="' + lang + '" sz="' + fontSize + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1834
|
+
'<a:r><a:rPr lang="' + lang + '" sz="' + fontSize + '" b="' + (bold ? 1 : 0) + '" dirty="0">' +
|
|
1823
1835
|
'<a:solidFill><a:srgbClr val="' + hex(color) + '"/></a:solidFill>' +
|
|
1824
|
-
'<a:latin typeface="' + escapeXml(font) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/></a:rPr>' +
|
|
1836
|
+
'<a:latin typeface="' + escapeXml(font) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/></a:rPr>' +
|
|
1825
1837
|
'<a:t>' + escapeXml(text) + '</a:t></a:r></a:p></a:txBody>' +
|
|
1826
1838
|
'<a:tcPr marL="91440" marR="91440" anchor="ctr">' +
|
|
1827
1839
|
(fill !== '' ? '<a:solidFill><a:srgbClr val="' + hex(fill) + '"/></a:solidFill>' : '<a:noFill/>') +
|
|
@@ -1859,17 +1871,17 @@ function slideShapeList(slide, index, total, theme, langId) {
|
|
|
1859
1871
|
const idBase = (index + 1) * 10
|
|
1860
1872
|
const footer = String(index + 1).padStart(2, '0') + ' / ' + String(total).padStart(2, '0')
|
|
1861
1873
|
|
|
1862
|
-
if (MEDIA_LAYOUTS.has(slide.layout)) {
|
|
1863
|
-
const boxes = mediaBoxes(slide.layout)
|
|
1864
|
-
const emu = box => Object.fromEntries(Object.entries(box).map(([key, value]) => [key, Math.round(value * 914400)]))
|
|
1865
|
-
shapes.push(textShapeXml(idBase + 2, 'Title', emu(boxes.title), [para(title, { size: 3400, color: p.fg, bold: true, font })]))
|
|
1866
|
-
if (boxes.body) shapes.push(textShapeXml(idBase + 3, 'Body', emu(boxes.body), [
|
|
1867
|
-
...(subtitle ? [para(subtitle, { size: 2000, color: p.fg, font: bodyFont })] : []),
|
|
1868
|
-
...bullets.map(bullet => para(bullet, { size: 2000, color: p.fg, bullet: true, font: bodyFont, spaceAfter: 600 })),
|
|
1869
|
-
]))
|
|
1870
|
-
const caption = slide.chart?.caption || slide.image?.caption
|
|
1871
|
-
if (caption) shapes.push(textShapeXml(idBase + 4, 'Caption', emu(boxes.caption), [para(caption, { size: 1400, color: p.muted, font: bodyFont })]))
|
|
1872
|
-
} else if (slide.layout === 'cover') {
|
|
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') {
|
|
1873
1885
|
shapes.push(accentBarXml(idBase + 1, 'Accent bar', { x: 914400, y: 2250000, w: 240000, h: 1600000 }, p.accent))
|
|
1874
1886
|
shapes.push(textShapeXml(idBase + 2, 'Title', { x: 1550000, y: 2160000, w: 9250000, h: 1800000 }, [
|
|
1875
1887
|
para(title, { size: 4400, color: p.fg, bold: true, font }),
|
|
@@ -1922,8 +1934,8 @@ function slideShapeList(slide, index, total, theme, langId) {
|
|
|
1922
1934
|
]))
|
|
1923
1935
|
}
|
|
1924
1936
|
} else if (slide.layout === 'table') {
|
|
1925
|
-
const hasTitle = title !== ''
|
|
1926
|
-
if (kicker !== '' && kicker !== title) {
|
|
1937
|
+
const hasTitle = title !== ''
|
|
1938
|
+
if (kicker !== '' && kicker !== title) {
|
|
1927
1939
|
shapes.push(textShapeXml(idBase + 1, 'Kicker', { x: 900000, y: 420000, w: 10000000, h: 420000 }, [
|
|
1928
1940
|
para(kicker, { size: 1400, color: p.accent, bold: true, font: theme.fonts.body }),
|
|
1929
1941
|
]))
|
|
@@ -2118,7 +2130,7 @@ function slideMasterXml(theme) {
|
|
|
2118
2130
|
const levels = Array.from({ length: 9 }, (_, i) => {
|
|
2119
2131
|
const sz = Math.max(1100, 2000 - i * 100)
|
|
2120
2132
|
return '<a:lvl' + (i + 1) + 'pPr marL="' + (342900 + i * 342900) + '" indent="' + (-342900 - i * 0) + '">' +
|
|
2121
|
-
'<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>'
|
|
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>'
|
|
2122
2134
|
}).join('')
|
|
2123
2135
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
2124
2136
|
'<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
@@ -2173,8 +2185,8 @@ function themeXml(theme) {
|
|
|
2173
2185
|
color('hlink', p.accent) + color('folHlink', p.accent2) +
|
|
2174
2186
|
'</a:clrScheme>' +
|
|
2175
2187
|
'<a:fontScheme name="dsh-ppt">' +
|
|
2176
|
-
'<a:majorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.heading)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.heading)) + '"/><a:cs typeface=""/></a:majorFont>' +
|
|
2177
|
-
'<a:minorFont><a:latin typeface="' + escapeXml(pptxFontName(theme.fonts.body)) + '"/><a:ea typeface="' + escapeXml(pptxEastAsianFont(theme.fonts.body)) + '"/><a:cs typeface=""/></a:minorFont>' +
|
|
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>' +
|
|
2178
2190
|
'</a:fontScheme>' +
|
|
2179
2191
|
'<a:fmtScheme name="dsh-ppt">' +
|
|
2180
2192
|
'<a:fillStyleLst>' +
|