dsh-ppt 0.4.4 → 0.4.5
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 +67 -60
- package/LICENSE +21 -21
- package/README.en.md +168 -168
- package/README.md +170 -170
- package/cordis.patch.yml +19 -19
- package/lib/execution.js +12 -1
- package/lib/tools.js +11 -3
- package/lib/types.d.ts +14 -0
- package/package.json +1 -1
- package/skills/dsh-ppt/SKILL.md +69 -69
- package/skills/dsh-ppt/references/copywriting.md +59 -59
- package/skills/dsh-ppt/references/syntax.md +44 -44
- package/skills/dsh-ppt/references/themes.md +61 -59
- package/skills/dsh-ppt/references/troubleshooting.md +13 -13
- package/skills/dsh-ppt/scripts/build-deck.mjs +135 -135
- package/skills/dsh-ppt/scripts/deck-core.mjs +738 -321
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
|
-
import { resolve as resolvePath } from 'node:path'
|
|
18
|
+
import { resolve as resolvePath, join as joinPath } from 'node:path'
|
|
19
19
|
import { deflateRawSync } from 'node:zlib'
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -152,13 +152,27 @@ export const LANGUAGES = {
|
|
|
152
152
|
pointKicker: '要点',
|
|
153
153
|
statementKicker: '核心观点',
|
|
154
154
|
quoteKicker: '金句',
|
|
155
|
-
tableKicker: '数据',
|
|
156
|
-
continuation: '(续)',
|
|
155
|
+
tableKicker: '数据',
|
|
156
|
+
continuation: '(续)',
|
|
157
157
|
notesLabel: '演讲者备注',
|
|
158
158
|
notesToggle: '备注',
|
|
159
159
|
closingTitle: '谢谢',
|
|
160
160
|
closingSubtitle: '讨论与问答',
|
|
161
|
-
generatedBy: '由 dsh-ppt 生成',
|
|
161
|
+
generatedBy: '由 dsh-ppt 生成',
|
|
162
|
+
overviewToggle: '总览',
|
|
163
|
+
presenterToggle: '演讲者',
|
|
164
|
+
presenterLabel: '演讲者视图 · Presenter view',
|
|
165
|
+
helpTitle: '快捷键 · Shortcuts',
|
|
166
|
+
noNotes: '(本页无备注)',
|
|
167
|
+
closeLabel: '关闭',
|
|
168
|
+
currentLabel: '当前页 · Current',
|
|
169
|
+
nextLabel: '下一页 · Next',
|
|
170
|
+
timerLabel: '计时 · Timer',
|
|
171
|
+
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示,观众看不到',
|
|
172
|
+
wrapHint: '已环绕到',
|
|
173
|
+
popupBlocked: '浏览器拦截了演讲者窗口,请允许弹出窗口后按 V 重试',
|
|
174
|
+
notesCloseLabel: '关闭备注',
|
|
175
|
+
overviewHint: '点击缩略图跳页,方向键切换,Enter 进入,Esc 退出'
|
|
162
176
|
},
|
|
163
177
|
},
|
|
164
178
|
en: {
|
|
@@ -173,13 +187,27 @@ export const LANGUAGES = {
|
|
|
173
187
|
pointKicker: 'Key point',
|
|
174
188
|
statementKicker: 'Core idea',
|
|
175
189
|
quoteKicker: 'Quote',
|
|
176
|
-
tableKicker: 'Data',
|
|
177
|
-
continuation: ' (continued)',
|
|
190
|
+
tableKicker: 'Data',
|
|
191
|
+
continuation: ' (continued)',
|
|
178
192
|
notesLabel: 'Speaker notes',
|
|
179
193
|
notesToggle: 'Notes',
|
|
180
194
|
closingTitle: 'Thank You',
|
|
181
195
|
closingSubtitle: 'Discussion & Q&A',
|
|
182
|
-
generatedBy: 'Generated with dsh-ppt',
|
|
196
|
+
generatedBy: 'Generated with dsh-ppt',
|
|
197
|
+
overviewToggle: 'Overview',
|
|
198
|
+
presenterToggle: 'Presenter',
|
|
199
|
+
presenterLabel: 'Presenter view',
|
|
200
|
+
helpTitle: 'Keyboard shortcuts',
|
|
201
|
+
noNotes: '(No notes on this slide)',
|
|
202
|
+
closeLabel: 'Close',
|
|
203
|
+
currentLabel: 'Current',
|
|
204
|
+
nextLabel: 'Next',
|
|
205
|
+
timerLabel: 'Timer',
|
|
206
|
+
presenterHint: 'Notes: S shows them on top until a presenter window is open; afterwards only in the presenter window.',
|
|
207
|
+
wrapHint: 'Wrapped to',
|
|
208
|
+
popupBlocked: 'The presenter window was blocked. Allow pop-ups and press V to retry.',
|
|
209
|
+
notesCloseLabel: 'Close notes',
|
|
210
|
+
overviewHint: 'Click a thumbnail to jump; arrows move, Enter opens, Esc exits'
|
|
183
211
|
},
|
|
184
212
|
},
|
|
185
213
|
bilingual: {
|
|
@@ -194,17 +222,140 @@ export const LANGUAGES = {
|
|
|
194
222
|
pointKicker: '要点 · Key point',
|
|
195
223
|
statementKicker: '核心观点 · Core Idea',
|
|
196
224
|
quoteKicker: '金句 · Quote',
|
|
197
|
-
tableKicker: '数据 · Data',
|
|
198
|
-
continuation: '(续 · continued)',
|
|
225
|
+
tableKicker: '数据 · Data',
|
|
226
|
+
continuation: '(续 · continued)',
|
|
199
227
|
notesLabel: '演讲者备注 · Speaker notes',
|
|
200
228
|
notesToggle: '备注 · Notes',
|
|
201
229
|
closingTitle: '谢谢 · Thank You',
|
|
202
230
|
closingSubtitle: '讨论与问答 · Q&A',
|
|
203
|
-
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
231
|
+
generatedBy: '由 dsh-ppt 生成 · Generated with dsh-ppt',
|
|
232
|
+
overviewToggle: '总览 · Overview',
|
|
233
|
+
presenterToggle: '演讲者 · Presenter',
|
|
234
|
+
presenterLabel: '演讲者视图 · Presenter view',
|
|
235
|
+
helpTitle: '快捷键 · Shortcuts',
|
|
236
|
+
noNotes: '(本页无备注 · No notes on this slide)',
|
|
237
|
+
closeLabel: '关闭 · Close',
|
|
238
|
+
currentLabel: '当前页 · Current',
|
|
239
|
+
nextLabel: '下一页 · Next',
|
|
240
|
+
timerLabel: '计时 · Timer',
|
|
241
|
+
presenterHint: '备注:未开演讲者窗口时按 S 显示在顶部;打开后只在演讲者窗口显示 · Notes: S shows on top until a presenter window is open',
|
|
242
|
+
wrapHint: '已环绕到 · Wrapped to',
|
|
243
|
+
popupBlocked: '浏览器拦截了演讲者窗口 · Presenter window blocked; allow pop-ups and press V',
|
|
244
|
+
notesCloseLabel: '关闭备注 · Close notes',
|
|
245
|
+
overviewHint: '点击缩略图跳页 · Click to jump, arrows move, Enter opens, Esc exits'
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// 主题预览(ppt_themes preview=true / buildThemePreview)
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
function themePages(lang) {
|
|
255
|
+
return lang === 'en'
|
|
256
|
+
? { title: 'dsh-ppt theme gallery', hint: 'Pick a theme id for ppt_create', usage: 'theme', dark: 'dark', light: 'light', palette: 'Palette' }
|
|
257
|
+
: { title: 'dsh-ppt 主题预览', hint: '把 id 填给 ppt_create 的 theme 参数', usage: 'theme', dark: '深色', light: '浅色', palette: '色板' }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 单套主题的 960x540 SVG 色板卡:可直接放进 README / npm 首屏。 */
|
|
261
|
+
export function renderThemeSvg(input, lang = 'zh') {
|
|
262
|
+
const theme = typeof input === 'string' ? resolveTheme(input) : input
|
|
263
|
+
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
264
|
+
const p = theme.palette
|
|
265
|
+
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
266
|
+
const font = theme.fonts.heading.replace(/"/g, "'")
|
|
267
|
+
const bodyFont = theme.fonts.body.replace(/"/g, "'")
|
|
268
|
+
const out = []
|
|
269
|
+
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)) + '">')
|
|
270
|
+
out.push('<rect width="960" height="540" fill="' + p.bg + '"/>')
|
|
271
|
+
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 + '"/>')
|
|
272
|
+
out.push('<rect x="36" y="40" width="560" height="360" rx="16" fill="' + p.panel + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
273
|
+
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>')
|
|
274
|
+
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>')
|
|
275
|
+
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) + '"/>')
|
|
276
|
+
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) + '"/>')
|
|
277
|
+
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) + '"/>')
|
|
278
|
+
out.push('<rect x="64" y="312" width="500" height="34" rx="6" fill="' + p.accent + '"/>')
|
|
279
|
+
out.push('<text x="78" y="334" fill="' + p.bg + '" font-family="' + escapeXml(bodyFont) + '" font-size="14" font-weight="700">dsh-ppt</text>')
|
|
280
|
+
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>')
|
|
281
|
+
out.push('<rect x="64" y="346" width="500" height="2" fill="' + hexToRgba(p.muted, 0.5) + '"/>')
|
|
282
|
+
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>')
|
|
283
|
+
const swatches = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
284
|
+
const labels = ['bg', 'panel', 'fg', 'muted', 'accent', 'accent2']
|
|
285
|
+
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>')
|
|
286
|
+
swatches.forEach((key, i) => {
|
|
287
|
+
const y = 96 + i * 62
|
|
288
|
+
out.push('<rect x="632" y="' + y + '" width="44" height="44" rx="10" fill="' + p[key] + '" stroke="' + hexToRgba(p.muted, 0.45) + '"/>')
|
|
289
|
+
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>')
|
|
290
|
+
out.push('<text x="692" y="' + (y + 38) + '" fill="' + p.muted + '" font-family="' + escapeXml(bodyFont) + '" font-size="13">' + escapeXml(String(p[key]).toUpperCase()) + '</text>')
|
|
291
|
+
})
|
|
292
|
+
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>')
|
|
293
|
+
out.push('</svg>')
|
|
294
|
+
return out.join('')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 自包含主题对比页:5 套主题并排,含色板与用法。 */
|
|
298
|
+
export function renderThemePreviewHtml(lang = 'zh') {
|
|
299
|
+
const L = themePages(lang === 'en' ? 'en' : 'zh')
|
|
300
|
+
const cards = THEME_IDS.map((id) => {
|
|
301
|
+
const theme = THEMES[id]
|
|
302
|
+
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
303
|
+
const chips = Object.keys(theme.palette).map((key) =>
|
|
304
|
+
'<span class="chip"><i style="background:' + theme.palette[key] + '"></i>' + escapeHtml(String(theme.palette[key]).toUpperCase()) + '</span>').join('')
|
|
305
|
+
return '<figure class="card" id="theme-' + escapeHtml(id) + '">' +
|
|
306
|
+
renderThemeSvg(theme, lang === 'en' ? 'en' : 'zh') +
|
|
307
|
+
'<figcaption><div class="row"><strong>' + escapeHtml(id) + '</strong><span class="badge">' + escapeHtml(pick(theme.name)) + '</span></div>' +
|
|
308
|
+
'<p class="muted">' + escapeHtml(pick(theme.mood)) + ' · ' + escapeHtml(pick(theme.bestFor)) + '</p>' +
|
|
309
|
+
'<p class="chips">' + chips + '</p>' +
|
|
310
|
+
'<p class="usage">' + escapeHtml(L.usage) + ': <code>' + escapeHtml(id) + '</code></p></figcaption></figure>'
|
|
311
|
+
}).join('')
|
|
312
|
+
return '<!DOCTYPE html>' + eolSafe('<html lang="zh-CN">') +
|
|
313
|
+
'<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">' +
|
|
314
|
+
'<title>' + escapeHtml(L.title) + '</title><style>' +
|
|
315
|
+
'body{margin:0;background:#0b0f17;color:#e8f1ff;font-family:"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;padding:28px}' +
|
|
316
|
+
'h1{font-size:26px;margin:0 0 6px}p.sub{color:#8b98ad;margin:0 0 24px;font-size:14px}' +
|
|
317
|
+
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));gap:22px}' +
|
|
318
|
+
'.card{margin:0;background:#111827;border:1px solid #263043;border-radius:16px;overflow:hidden}' +
|
|
319
|
+
'.card svg{display:block;width:100%;height:auto}' +
|
|
320
|
+
'figcaption{padding:14px 16px 16px}.row{display:flex;align-items:center;gap:10px;font-size:16px}' +
|
|
321
|
+
'.badge{color:#8b98ad;font-size:13px}.muted{color:#8b98ad;font-size:13px;margin:6px 0 10px;line-height:1.5}' +
|
|
322
|
+
'.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}' +
|
|
323
|
+
'.chip i{width:10px;height:10px;border-radius:3px;display:inline-block}' +
|
|
324
|
+
'.usage{margin:0;font-size:13px;color:#aab6c9}code{background:#0b0f17;border:1px solid #263043;border-radius:6px;padding:1px 6px}' +
|
|
325
|
+
'@media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}' +
|
|
326
|
+
'</style></head><body><h1>' + escapeHtml(L.title) + '</h1><p class="sub">' + escapeHtml(L.hint) + ' · dsh-ppt v' + escapeHtml(DECK_VERSION) + '</p>' +
|
|
327
|
+
'<div class="grid">' + cards + '</div></body></html>'
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function eolSafe(text) { return text }
|
|
331
|
+
|
|
332
|
+
/** 写主题对比页 + 每套主题 SVG 色板卡;默认不覆盖同名文件。 */
|
|
333
|
+
export function buildThemePreview(options = {}) {
|
|
334
|
+
const langInput = typeof options.lang === 'string' && options.lang.trim() !== '' ? options.lang.trim() : 'zh'
|
|
335
|
+
const language = resolveLanguage(langInput)
|
|
336
|
+
const outputDir = resolvePath(String(options.outputDir ?? '.'))
|
|
337
|
+
mkdirSync(outputDir, { recursive: true })
|
|
338
|
+
const overwrite = options.overwrite === true
|
|
339
|
+
const pickPath = (base, ext) => {
|
|
340
|
+
const first = resolvePath(joinPath(outputDir, base + ext))
|
|
341
|
+
if (overwrite || !existsSync(first)) return first
|
|
342
|
+
for (let i = 1; i < 1000; i += 1) {
|
|
343
|
+
const candidate = resolvePath(joinPath(outputDir, base + '-' + i + ext))
|
|
344
|
+
if (!existsSync(candidate)) return candidate
|
|
345
|
+
}
|
|
346
|
+
throw new Error('dsh-ppt:找不到可用的预览文件名(同名前缀超过 999 个),请允许 overwrite。')
|
|
347
|
+
}
|
|
348
|
+
const htmlPath = pickPath('themes-preview', '.html')
|
|
349
|
+
writeFileSync(htmlPath, renderThemePreviewHtml(language.id), 'utf8')
|
|
350
|
+
const svgs = THEME_IDS.map((id) => {
|
|
351
|
+
const svgPath = pickPath('theme-' + id, '.svg')
|
|
352
|
+
writeFileSync(svgPath, renderThemeSvg(id, language.id), 'utf8')
|
|
353
|
+
return { id, path: svgPath }
|
|
354
|
+
})
|
|
355
|
+
return { ok: true, outputDir, htmlPath, svgs, themeCount: THEME_IDS.length, language: language.id }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
208
359
|
export function resolveTheme(input) {
|
|
209
360
|
const id = String(input ?? DEFAULT_THEME).trim().toLowerCase()
|
|
210
361
|
const theme = THEMES[id]
|
|
@@ -371,37 +522,37 @@ function quoteSlideFrom(section, ui) {
|
|
|
371
522
|
return { layout: 'quote', kicker: ui.quoteKicker, title: truncate(lines.join(' '), 220), subtitle: attribution }
|
|
372
523
|
}
|
|
373
524
|
|
|
374
|
-
function paginateTable(slide, continuation = '(续)') {
|
|
375
|
-
const rows = slide.rows
|
|
376
|
-
const columns = Math.max(...rows.map(row => row.length))
|
|
377
|
-
if (columns > 8) throw new Error('dsh-ppt:表格最多 8 列,请拆成多个表格后重试 / tables support up to 8 columns')
|
|
378
|
-
if (rows.some(row => row.some(cell => [...cell].length > 60))) {
|
|
379
|
-
throw new Error('dsh-ppt:表格单元格最多 60 个字符,请精简或拆分内容后重试 / cells support up to 60 characters')
|
|
380
|
-
}
|
|
381
|
-
const padded = rows.map(row => Array.from({ length: columns }, (_, i) => row[i] ?? ''))
|
|
382
|
-
const pages = chunk(padded.slice(1), 8)
|
|
383
|
-
if (pages.length === 0) pages.push([])
|
|
384
|
-
return pages.map((body, index) => {
|
|
385
|
-
const { notes, ...rest } = slide
|
|
386
|
-
const title = slide.title + (index === 0 ? '' : continuation)
|
|
387
|
-
// 同名 kicker/title 共用紧凑标题;续页不能凭空多出一行大标题挤压表格。
|
|
388
|
-
return { ...rest, title, kicker: slide.kicker === slide.title ? title : slide.kicker, rows: [padded[0], ...body],
|
|
389
|
-
...(index === 0 && notes ? { notes } : {}) }
|
|
390
|
-
})
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function tableSlidesFrom(section, ui) {
|
|
394
|
-
const table = section.table
|
|
395
|
-
const rows = [table.header, ...table.rows]
|
|
396
|
-
const heading = section.heading || ui.tableKicker
|
|
397
|
-
return paginateTable({ layout: 'table', kicker: heading, title: heading, rows }, ui.continuation)
|
|
525
|
+
function paginateTable(slide, continuation = '(续)') {
|
|
526
|
+
const rows = slide.rows
|
|
527
|
+
const columns = Math.max(...rows.map(row => row.length))
|
|
528
|
+
if (columns > 8) throw new Error('dsh-ppt:表格最多 8 列,请拆成多个表格后重试 / tables support up to 8 columns')
|
|
529
|
+
if (rows.some(row => row.some(cell => [...cell].length > 60))) {
|
|
530
|
+
throw new Error('dsh-ppt:表格单元格最多 60 个字符,请精简或拆分内容后重试 / cells support up to 60 characters')
|
|
531
|
+
}
|
|
532
|
+
const padded = rows.map(row => Array.from({ length: columns }, (_, i) => row[i] ?? ''))
|
|
533
|
+
const pages = chunk(padded.slice(1), 8)
|
|
534
|
+
if (pages.length === 0) pages.push([])
|
|
535
|
+
return pages.map((body, index) => {
|
|
536
|
+
const { notes, ...rest } = slide
|
|
537
|
+
const title = slide.title + (index === 0 ? '' : continuation)
|
|
538
|
+
// 同名 kicker/title 共用紧凑标题;续页不能凭空多出一行大标题挤压表格。
|
|
539
|
+
return { ...rest, title, kicker: slide.kicker === slide.title ? title : slide.kicker, rows: [padded[0], ...body],
|
|
540
|
+
...(index === 0 && notes ? { notes } : {}) }
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function tableSlidesFrom(section, ui) {
|
|
545
|
+
const table = section.table
|
|
546
|
+
const rows = [table.header, ...table.rows]
|
|
547
|
+
const heading = section.heading || ui.tableKicker
|
|
548
|
+
return paginateTable({ layout: 'table', kicker: heading, title: heading, rows }, ui.continuation)
|
|
398
549
|
}
|
|
399
550
|
|
|
400
551
|
/** 把一个 section 里的金句/表格拆成独立页;备注附着到第一张产出页。 */
|
|
401
552
|
function specialSlidesFor(section, ui) {
|
|
402
553
|
const out = []
|
|
403
554
|
if (section.quote.length > 0) out.push(quoteSlideFrom(section, ui))
|
|
404
|
-
if (section.table !== null) out.push(...tableSlidesFrom(section, ui))
|
|
555
|
+
if (section.table !== null) out.push(...tableSlidesFrom(section, ui))
|
|
405
556
|
if (out.length > 0 && section.notes.length > 0) out[0].notes = section.notes.join('\n')
|
|
406
557
|
return out
|
|
407
558
|
}
|
|
@@ -425,8 +576,8 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
425
576
|
let firstH1Seen = false
|
|
426
577
|
|
|
427
578
|
const flush = () => {
|
|
428
|
-
if (current !== null && (current.heading !== '' || current.bullets.length > 0 || current.paragraphs.length > 0
|
|
429
|
-
|| current.table !== null || current.quote.length > 0)) {
|
|
579
|
+
if (current !== null && (current.heading !== '' || current.bullets.length > 0 || current.paragraphs.length > 0
|
|
580
|
+
|| current.table !== null || current.quote.length > 0)) {
|
|
430
581
|
sections.push(current)
|
|
431
582
|
}
|
|
432
583
|
current = null
|
|
@@ -481,7 +632,7 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
481
632
|
if (isTableSeparator(tableCells)) continue
|
|
482
633
|
if (current.table === null) {
|
|
483
634
|
current.table = { header: tableCells.map((cell) => stripInlineMarkdown(cell)), rows: [] }
|
|
484
|
-
} else {
|
|
635
|
+
} else {
|
|
485
636
|
current.table.rows.push(tableCells.map((cell) => stripInlineMarkdown(cell)))
|
|
486
637
|
}
|
|
487
638
|
continue
|
|
@@ -515,7 +666,7 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
515
666
|
}
|
|
516
667
|
}
|
|
517
668
|
|
|
518
|
-
let bodySections = sections.filter((section) => section.coverOnly !== true)
|
|
669
|
+
let bodySections = sections.filter((section) => section.coverOnly !== true)
|
|
519
670
|
// 首个 H1 既是封面又把所有要点收在封面节里时,把封面副标题之外的剩余要点
|
|
520
671
|
// 提升为一个无标题节,走下面的要点页生成逻辑,避免丢掉内容。
|
|
521
672
|
if (bodySections.length === 0 && coverSource !== null && coverSource !== undefined) {
|
|
@@ -527,18 +678,18 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
527
678
|
if (extras.length > 0) {
|
|
528
679
|
const synthetic = createSection()
|
|
529
680
|
synthetic.bullets = extras
|
|
530
|
-
bodySections = [synthetic]
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// 表格和引用不能只挂在封面节上,否则首个 H1 后没有 H2 时会丢失。
|
|
535
|
-
if (coverSource && (coverSource.table !== null || coverSource.quote.length > 0)) {
|
|
536
|
-
const special = createSection()
|
|
537
|
-
special.table = coverSource.table
|
|
538
|
-
special.quote = coverSource.quote
|
|
539
|
-
special.notes = coverSource.notes
|
|
540
|
-
bodySections.unshift(special)
|
|
541
|
-
}
|
|
681
|
+
bodySections = [synthetic]
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 表格和引用不能只挂在封面节上,否则首个 H1 后没有 H2 时会丢失。
|
|
686
|
+
if (coverSource && (coverSource.table !== null || coverSource.quote.length > 0)) {
|
|
687
|
+
const special = createSection()
|
|
688
|
+
special.table = coverSource.table
|
|
689
|
+
special.quote = coverSource.quote
|
|
690
|
+
special.notes = coverSource.notes
|
|
691
|
+
bodySections.unshift(special)
|
|
692
|
+
}
|
|
542
693
|
|
|
543
694
|
if (bodySections.length === 0) {
|
|
544
695
|
// 一句话 / 无结构输入:封面 + 核心观点 + 结束页(仍是完整的三页演示文稿)
|
|
@@ -625,7 +776,7 @@ export function parseMarkdownDeck(titleInput, content, lang = 'zh') {
|
|
|
625
776
|
if (pages.length > 0) {
|
|
626
777
|
// 超过 8 条自动续页,不再静默丢弃;备注只挂在第一页
|
|
627
778
|
pages.forEach((pagePoints, pageIndex) => {
|
|
628
|
-
const label = pageIndex === 0 ? section.heading : section.heading + ui.continuation
|
|
779
|
+
const label = pageIndex === 0 ? section.heading : section.heading + ui.continuation
|
|
629
780
|
const slide = { layout: 'bullets', kicker: label, title: label, bullets: pagePoints }
|
|
630
781
|
if (pageIndex === 0 && specials.length === 0 && notes !== '') slide.notes = notes
|
|
631
782
|
slides.push(slide)
|
|
@@ -669,8 +820,8 @@ function normalizeSlide(raw, index) {
|
|
|
669
820
|
let rows = []
|
|
670
821
|
if (Array.isArray(source.rows)) {
|
|
671
822
|
rows = source.rows
|
|
672
|
-
.map((row) => (Array.isArray(row) ? row : [row]).map((cell) => stripInlineMarkdown(String(cell))))
|
|
673
|
-
.filter((row) => row.some((cell) => cell !== ''))
|
|
823
|
+
.map((row) => (Array.isArray(row) ? row : [row]).map((cell) => stripInlineMarkdown(String(cell))))
|
|
824
|
+
.filter((row) => row.some((cell) => cell !== ''))
|
|
674
825
|
}
|
|
675
826
|
if (rows.length === 0) {
|
|
676
827
|
// 没有表格数据的 table 页退化成要点页,避免产出空白版式
|
|
@@ -681,22 +832,22 @@ function normalizeSlide(raw, index) {
|
|
|
681
832
|
return { layout, kicker, title, subtitle, text, bullets, ...(notes !== '' ? { notes } : {}) }
|
|
682
833
|
}
|
|
683
834
|
|
|
684
|
-
function checkSlideLimit(slides, limit) {
|
|
685
|
-
if (slides.length > limit) {
|
|
686
|
-
throw new Error('dsh-ppt:生成后共 ' + slides.length + ' 页,超过 maxSlides=' + limit
|
|
687
|
-
+ ',请提高 maxSlides(最多 120)或拆分内容 / generated ' + slides.length + ' slides; raise maxSlides or split the deck')
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
export function normalizeSlides(rawSlides, maxSlides = 60, lang = 'zh') {
|
|
835
|
+
function checkSlideLimit(slides, limit) {
|
|
836
|
+
if (slides.length > limit) {
|
|
837
|
+
throw new Error('dsh-ppt:生成后共 ' + slides.length + ' 页,超过 maxSlides=' + limit
|
|
838
|
+
+ ',请提高 maxSlides(最多 120)或拆分内容 / generated ' + slides.length + ' slides; raise maxSlides or split the deck')
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function normalizeSlides(rawSlides, maxSlides = 60, lang = 'zh') {
|
|
692
843
|
if (!Array.isArray(rawSlides) || rawSlides.length === 0) {
|
|
693
844
|
throw new Error('dsh-ppt:slides 必须是非空数组(每个元素是 { layout, title, subtitle, kicker, bullets } 对象)')
|
|
694
845
|
}
|
|
695
846
|
const limit = clampInt(maxSlides, 60, 1, 120)
|
|
696
|
-
const ui = resolveLanguage(lang).ui
|
|
697
|
-
const slides = rawSlides.map(normalizeSlide).flatMap(slide => slide.layout === 'table'
|
|
698
|
-
? paginateTable(slide, ui.continuation) : [slide])
|
|
699
|
-
checkSlideLimit(slides, limit)
|
|
847
|
+
const ui = resolveLanguage(lang).ui
|
|
848
|
+
const slides = rawSlides.map(normalizeSlide).flatMap(slide => slide.layout === 'table'
|
|
849
|
+
? paginateTable(slide, ui.continuation) : [slide])
|
|
850
|
+
checkSlideLimit(slides, limit)
|
|
700
851
|
if (slides.length === 0) throw new Error('dsh-ppt:slides 规范化后为空')
|
|
701
852
|
return slides
|
|
702
853
|
}
|
|
@@ -717,13 +868,13 @@ export function normalizeBuildOptions(options = {}) {
|
|
|
717
868
|
deck = {
|
|
718
869
|
title,
|
|
719
870
|
subtitle: stripInlineMarkdown(options.subtitle ?? ''),
|
|
720
|
-
slides: normalizeSlides(options.slides, maxSlides, language.id),
|
|
871
|
+
slides: normalizeSlides(options.slides, maxSlides, language.id),
|
|
721
872
|
}
|
|
722
873
|
} else {
|
|
723
874
|
const content = String(options.content ?? '').trim()
|
|
724
875
|
if (content === '') throw new Error('dsh-ppt:content 不能为空(或用 slides 传结构化幻灯片)')
|
|
725
876
|
deck = parseMarkdownDeck(title, content, language.id)
|
|
726
|
-
checkSlideLimit(deck.slides, maxSlides)
|
|
877
|
+
checkSlideLimit(deck.slides, maxSlides)
|
|
727
878
|
}
|
|
728
879
|
if (deck.slides.length < 1) throw new Error('dsh-ppt:没有可生成的幻灯片')
|
|
729
880
|
const outputDir = resolvePath(String(options.outputDir ?? '.').trim() || '.')
|
|
@@ -838,251 +989,514 @@ export function buildDeck(options = {}) {
|
|
|
838
989
|
// HTML 网页放映
|
|
839
990
|
// ---------------------------------------------------------------------------
|
|
840
991
|
|
|
841
|
-
export function renderHtml(manifest, theme, language) {
|
|
842
|
-
const t = theme
|
|
843
|
-
const lang = language
|
|
844
|
-
const ui = lang.ui
|
|
845
|
-
const motion = manifest.motion !== false
|
|
846
|
-
const motionCss = motion
|
|
847
|
-
? `body.motion .slide.is-active{animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
848
|
-
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
849
|
-
body.motion .bullets li{
|
|
850
|
-
opacity:0;animation:bullet-in .55s cubic-bezier(.22,.8,.36,1) forwards;
|
|
851
|
-
animation-delay:calc(var(--i, 0)*90ms + .12s);
|
|
852
|
-
}
|
|
853
|
-
@keyframes bullet-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}`
|
|
854
|
-
: ''
|
|
855
|
-
const slides = manifest.slides.map((slide, index) =>
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
<
|
|
865
|
-
<
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
--
|
|
873
|
-
--
|
|
874
|
-
--
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
.slide
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
.
|
|
904
|
-
color:var(--
|
|
905
|
-
|
|
906
|
-
}
|
|
907
|
-
.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
.
|
|
928
|
-
.
|
|
929
|
-
.
|
|
930
|
-
.
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
.
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
#hud
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
body.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
(()
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
})
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
</
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
992
|
+
export function renderHtml(manifest, theme, language) {
|
|
993
|
+
const t = theme
|
|
994
|
+
const lang = language
|
|
995
|
+
const ui = lang.ui
|
|
996
|
+
const motion = manifest.motion !== false
|
|
997
|
+
const motionCss = motion
|
|
998
|
+
? `body.motion .slide.is-active{animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
999
|
+
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
1000
|
+
body.motion .bullets li{
|
|
1001
|
+
opacity:0;animation:bullet-in .55s cubic-bezier(.22,.8,.36,1) forwards;
|
|
1002
|
+
animation-delay:calc(var(--i, 0)*90ms + .12s);
|
|
1003
|
+
}
|
|
1004
|
+
@keyframes bullet-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}`
|
|
1005
|
+
: ''
|
|
1006
|
+
const slides = manifest.slides.map((slide, index) => {
|
|
1007
|
+
const label = slide.title || slide.kicker || (ui.slide + ' ' + (index + 1))
|
|
1008
|
+
return '<div class="slide-frame" data-frame="' + (index + 1) + '" tabindex="-1" role="button" aria-label="' + escapeHtml(String(label)) + '">' +
|
|
1009
|
+
renderHtmlSlide(slide, index, ui, lang.id) + '</div>'
|
|
1010
|
+
}).join('\n')
|
|
1011
|
+
const themeLabel = t.name[lang.id] ?? t.name.en
|
|
1012
|
+
const total = manifest.slides.length
|
|
1013
|
+
|
|
1014
|
+
return `<!DOCTYPE html>
|
|
1015
|
+
<html lang="${lang.attr}">
|
|
1016
|
+
<head>
|
|
1017
|
+
<meta charset="UTF-8">
|
|
1018
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1019
|
+
<link rel="icon" href="data:,">
|
|
1020
|
+
<title>${escapeHtml(manifest.title)}</title>
|
|
1021
|
+
<style>
|
|
1022
|
+
:root{
|
|
1023
|
+
--bg:${t.palette.bg};
|
|
1024
|
+
--panel:${t.palette.panel};
|
|
1025
|
+
--fg:${t.palette.fg};
|
|
1026
|
+
--muted:${t.palette.muted};
|
|
1027
|
+
--accent:${t.palette.accent};
|
|
1028
|
+
--accent2:${t.palette.accent2};
|
|
1029
|
+
--font-heading:${t.fonts.heading};
|
|
1030
|
+
--font-body:${t.fonts.body};
|
|
1031
|
+
}
|
|
1032
|
+
*{box-sizing:border-box}
|
|
1033
|
+
html,body{height:100%}
|
|
1034
|
+
body{
|
|
1035
|
+
margin:0;background:var(--bg);color:var(--fg);
|
|
1036
|
+
font-family:var(--font-body);overflow:hidden;
|
|
1037
|
+
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
|
|
1038
|
+
}
|
|
1039
|
+
#stage{position:fixed;inset:0}
|
|
1040
|
+
.slide-frame{position:fixed;inset:0}
|
|
1041
|
+
.slide{
|
|
1042
|
+
position:absolute;inset:0;display:none;flex-direction:column;justify-content:center;
|
|
1043
|
+
padding:clamp(34px,7vw,110px);overflow:hidden;
|
|
1044
|
+
}
|
|
1045
|
+
.slide::before{
|
|
1046
|
+
content:"";position:absolute;inset:-20%;pointer-events:none;z-index:-1;
|
|
1047
|
+
background:
|
|
1048
|
+
radial-gradient(42% 34% at 82% 18%, ${hexToRgba(t.palette.accent, t.dark ? 0.22 : 0.12)}, transparent 70%),
|
|
1049
|
+
radial-gradient(36% 30% at 12% 86%, ${hexToRgba(t.palette.accent2, t.dark ? 0.16 : 0.12)}, transparent 70%),
|
|
1050
|
+
radial-gradient(70% 60% at 50% 50%, ${hexToRgba(t.palette.panel, 0.55)}, transparent 100%);
|
|
1051
|
+
}
|
|
1052
|
+
.slide.is-active{display:flex}
|
|
1053
|
+
${motionCss}
|
|
1054
|
+
.kicker{
|
|
1055
|
+
color:var(--accent);font-weight:700;letter-spacing:.18em;text-transform:uppercase;
|
|
1056
|
+
font-size:clamp(12px,1.3vw,18px);margin-bottom:22px;
|
|
1057
|
+
}
|
|
1058
|
+
h1,h2,.statement-title{font-family:var(--font-heading);line-height:1.06;letter-spacing:-.015em;margin:0}
|
|
1059
|
+
h1{font-size:clamp(44px,7.4vw,118px);max-width:20ch}
|
|
1060
|
+
h2{font-size:clamp(34px,5vw,82px);max-width:20ch}
|
|
1061
|
+
.subtitle{
|
|
1062
|
+
color:var(--muted);font-size:clamp(18px,2.3vw,34px);line-height:1.5;
|
|
1063
|
+
max-width:46em;margin-top:28px;
|
|
1064
|
+
}
|
|
1065
|
+
.meta{
|
|
1066
|
+
color:var(--muted);font-size:clamp(12px,1.2vw,16px);margin-top:48px;
|
|
1067
|
+
letter-spacing:.06em;
|
|
1068
|
+
}
|
|
1069
|
+
.bullets ul{margin:30px 0 0;padding:0;list-style:none;display:grid;gap:clamp(12px,1.6vw,24px)}
|
|
1070
|
+
.bullets li{
|
|
1071
|
+
position:relative;padding-left:clamp(28px,2.6vw,44px);
|
|
1072
|
+
font-size:clamp(20px,2.6vw,40px);line-height:1.35;max-width:24em;
|
|
1073
|
+
}
|
|
1074
|
+
.bullets li::before{
|
|
1075
|
+
content:"";position:absolute;left:0;top:.58em;width:.5em;height:.5em;
|
|
1076
|
+
background:var(--accent);border-radius:2px;box-shadow:.28em .28em 0 color-mix(in srgb, var(--accent2) 78%, transparent);
|
|
1077
|
+
}
|
|
1078
|
+
.section .kicker{margin-bottom:10px}
|
|
1079
|
+
.section .accent-line{width:min(180px,18vw);height:6px;background:var(--accent);margin:28px 0}
|
|
1080
|
+
.statement-title{
|
|
1081
|
+
font-size:clamp(34px,5.4vw,88px);max-width:22ch;font-weight:800;
|
|
1082
|
+
border-left:6px solid var(--accent);padding-left:clamp(22px,3vw,48px);
|
|
1083
|
+
}
|
|
1084
|
+
.closing{text-align:center;align-items:center}
|
|
1085
|
+
.closing h1,.closing .statement-title{font-size:clamp(52px,9vw,148px)}
|
|
1086
|
+
.closing .subtitle{color:var(--accent);font-weight:700}
|
|
1087
|
+
.cover h1{font-weight:900}
|
|
1088
|
+
.quote-mark{
|
|
1089
|
+
font-family:var(--font-heading);font-size:clamp(90px,13vw,210px);line-height:.62;
|
|
1090
|
+
color:var(--accent);margin-bottom:clamp(14px,2vw,30px);
|
|
1091
|
+
}
|
|
1092
|
+
.quote-text{
|
|
1093
|
+
font-family:var(--font-heading);font-size:clamp(26px,4.2vw,62px);line-height:1.28;
|
|
1094
|
+
font-weight:700;max-width:22ch;margin:0;
|
|
1095
|
+
}
|
|
1096
|
+
.quote-attr{color:var(--muted);margin-top:clamp(18px,2.6vw,34px);font-size:clamp(15px,1.8vw,26px);letter-spacing:.04em}
|
|
1097
|
+
table.deck-table{
|
|
1098
|
+
border-collapse:collapse;margin-top:clamp(20px,3vw,42px);width:100%;max-width:100%;
|
|
1099
|
+
font-size:clamp(14px,1.55vw,23px);
|
|
1100
|
+
}
|
|
1101
|
+
.deck-table th{
|
|
1102
|
+
background:color-mix(in srgb, var(--accent) 26%, var(--bg));color:var(--fg);
|
|
1103
|
+
text-align:left;font-weight:700;padding:.6em .9em;letter-spacing:.02em;
|
|
1104
|
+
border-bottom:2px solid var(--accent);
|
|
1105
|
+
}
|
|
1106
|
+
.deck-table td{
|
|
1107
|
+
padding:.55em .9em;border-bottom:1px solid color-mix(in srgb, var(--muted) 55%, transparent);
|
|
1108
|
+
color:var(--fg);
|
|
1109
|
+
}
|
|
1110
|
+
.deck-table .num{text-align:right;font-variant-numeric:tabular-nums}
|
|
1111
|
+
.deck-table tbody tr:nth-child(even){background:color-mix(in srgb, var(--panel) 62%, transparent)}
|
|
1112
|
+
#progress{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);z-index:30;transition:width .25s}
|
|
1113
|
+
#hud{
|
|
1114
|
+
position:fixed;right:22px;bottom:18px;z-index:50;display:flex;gap:10px;align-items:center;
|
|
1115
|
+
color:var(--muted);font-size:13px;letter-spacing:.08em;font-variant-numeric:tabular-nums;
|
|
1116
|
+
}
|
|
1117
|
+
#hud button{
|
|
1118
|
+
background:color-mix(in srgb, var(--panel) 88%, transparent);color:var(--fg);
|
|
1119
|
+
border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);border-radius:99px;
|
|
1120
|
+
padding:8px 14px;min-height:34px;font:inherit;cursor:pointer;
|
|
1121
|
+
}
|
|
1122
|
+
#hud button:hover{border-color:var(--accent);color:var(--accent)}
|
|
1123
|
+
#notes-panel{
|
|
1124
|
+
position:fixed;left:0;right:0;bottom:64px;z-index:40;display:none;
|
|
1125
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);
|
|
1126
|
+
border-top:2px solid var(--accent);
|
|
1127
|
+
padding:14px clamp(22px,4vw,64px) 18px;max-height:38vh;overflow:auto;
|
|
1128
|
+
}
|
|
1129
|
+
body.notes-open #notes-panel{display:block}
|
|
1130
|
+
#notes-panel .notes-head{display:flex;align-items:center;justify-content:space-between;gap:16px}
|
|
1131
|
+
#notes-close{
|
|
1132
|
+
background:transparent;color:var(--muted);border:1px solid color-mix(in srgb, var(--muted) 45%, transparent);
|
|
1133
|
+
border-radius:99px;padding:4px 12px;font:inherit;cursor:pointer;min-height:30px;
|
|
1134
|
+
}
|
|
1135
|
+
#notes-close:hover{color:var(--accent);border-color:var(--accent)}
|
|
1136
|
+
#notes-panel .notes-label{
|
|
1137
|
+
color:var(--accent);font-weight:700;letter-spacing:.14em;text-transform:uppercase;
|
|
1138
|
+
font-size:12px;margin-bottom:8px;
|
|
1139
|
+
}
|
|
1140
|
+
#notes-text{white-space:pre-wrap;line-height:1.65;font-size:clamp(15px,1.5vw,18px);color:var(--fg)}
|
|
1141
|
+
#notes-text.is-empty{color:var(--muted);font-style:italic}
|
|
1142
|
+
#help-panel{
|
|
1143
|
+
position:fixed;right:22px;bottom:64px;z-index:60;max-width:390px;
|
|
1144
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);color:var(--fg);
|
|
1145
|
+
border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);border-radius:14px;
|
|
1146
|
+
padding:16px 20px;box-shadow:0 18px 50px rgba(0,0,0,.45);font-size:14px;line-height:1.8;
|
|
1147
|
+
}
|
|
1148
|
+
#help-panel[hidden]{display:none}
|
|
1149
|
+
#help-panel kbd{
|
|
1150
|
+
background:color-mix(in srgb, var(--accent) 22%, transparent);border-radius:5px;
|
|
1151
|
+
padding:1px 6px;font-family:inherit;font-size:12px;
|
|
1152
|
+
}
|
|
1153
|
+
#toast{
|
|
1154
|
+
position:fixed;left:50%;bottom:84px;transform:translateX(-50%);z-index:70;
|
|
1155
|
+
background:color-mix(in srgb, var(--panel) 96%, black 4%);color:var(--fg);
|
|
1156
|
+
border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);border-radius:99px;
|
|
1157
|
+
padding:9px 18px;font-size:13px;
|
|
1158
|
+
}
|
|
1159
|
+
#toast[hidden]{display:none}
|
|
1160
|
+
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
1161
|
+
body.overview #stage{
|
|
1162
|
+
display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px;
|
|
1163
|
+
padding:18px 18px 92px;position:fixed;inset:0;overflow:auto;align-content:start;
|
|
1164
|
+
}
|
|
1165
|
+
body.overview .slide-frame{
|
|
1166
|
+
position:relative;inset:auto;width:100%;aspect-ratio:16/9;overflow:hidden;
|
|
1167
|
+
border:1px solid color-mix(in srgb, var(--muted) 35%, transparent);border-radius:12px;
|
|
1168
|
+
cursor:pointer;background:var(--bg);outline:none;
|
|
1169
|
+
}
|
|
1170
|
+
body.overview .slide-frame:hover{border-color:var(--accent)}
|
|
1171
|
+
body.overview .slide-frame.is-current{outline:2px solid var(--accent);outline-offset:2px}
|
|
1172
|
+
body.overview .slide-frame:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
|
|
1173
|
+
body.overview .slide-frame::after{
|
|
1174
|
+
content:attr(data-frame);position:absolute;right:8px;bottom:6px;z-index:2;
|
|
1175
|
+
font-size:12px;color:var(--muted);background:color-mix(in srgb, var(--bg) 84%, transparent);
|
|
1176
|
+
border:1px solid color-mix(in srgb, var(--muted) 40%, transparent);border-radius:6px;
|
|
1177
|
+
padding:1px 7px;font-variant-numeric:tabular-nums;
|
|
1178
|
+
}
|
|
1179
|
+
body.overview .slide{
|
|
1180
|
+
display:flex !important;position:absolute;inset:0;width:100vw;height:100vh;
|
|
1181
|
+
transform:scale(var(--deck-thumb-scale,.25));transform-origin:top left;pointer-events:none;
|
|
1182
|
+
animation:none !important;
|
|
1183
|
+
}
|
|
1184
|
+
body.overview .bullets li{opacity:1 !important;animation:none !important}
|
|
1185
|
+
body.overview #notes-panel,body.presenting #notes-panel{display:none !important}
|
|
1186
|
+
body.presenting #notes-toggle{opacity:.55}
|
|
1187
|
+
body.presenting #hud{opacity:0;pointer-events:none;transition:opacity .25s}
|
|
1188
|
+
body.presenting #hud:hover{opacity:1;pointer-events:auto}
|
|
1189
|
+
@media (max-width:900px){
|
|
1190
|
+
.slide{padding:clamp(22px,5vw,44px)}
|
|
1191
|
+
h1{font-size:clamp(34px,8.5vw,64px)}
|
|
1192
|
+
h2{font-size:clamp(26px,6.5vw,48px)}
|
|
1193
|
+
.statement-title{font-size:clamp(26px,7vw,54px)}
|
|
1194
|
+
.subtitle{font-size:clamp(16px,3.4vw,24px)}
|
|
1195
|
+
.bullets li{font-size:clamp(16px,3.4vw,24px)}
|
|
1196
|
+
.kicker{font-size:13px}
|
|
1197
|
+
.deck-table{font-size:14px}
|
|
1198
|
+
.deck-table th,.deck-table td{padding:.55em .6em}
|
|
1199
|
+
#hud{right:10px;bottom:10px;gap:8px;font-size:12px}
|
|
1200
|
+
#hud button{padding:10px 16px;min-height:44px}
|
|
1201
|
+
#notes-panel{bottom:68px;max-height:45vh}
|
|
1202
|
+
#help-panel{right:10px;bottom:68px;left:10px;max-width:none}
|
|
1203
|
+
}
|
|
1204
|
+
@media (max-width:640px){
|
|
1205
|
+
#theme-label{display:none}
|
|
1206
|
+
}
|
|
1207
|
+
@media (prefers-reduced-motion: reduce){
|
|
1208
|
+
*,*::before,*::after{animation:none !important;transition:none !important}
|
|
1209
|
+
}
|
|
1210
|
+
@media print{
|
|
1211
|
+
html,body{height:auto;overflow:visible;background:#fff}
|
|
1212
|
+
.slide-frame{position:relative;display:block !important}
|
|
1213
|
+
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
1214
|
+
${motion ? 'body.motion .bullets li{opacity:1 !important;animation:none !important}' : ''}
|
|
1215
|
+
#progress,#hud,#notes-panel,#help-panel,#toast{display:none !important}
|
|
1216
|
+
}
|
|
1217
|
+
</style>
|
|
1218
|
+
</head>
|
|
1219
|
+
<body class="${motion ? 'motion' : 'no-motion'}">
|
|
1220
|
+
<div id="progress" aria-hidden="true"></div>
|
|
1221
|
+
<div id="stage">
|
|
1222
|
+
${slides}
|
|
1223
|
+
</div>
|
|
1224
|
+
<div id="hud" aria-live="polite">
|
|
1225
|
+
<span id="counter">${ui.slide} 1 ${ui.of} ${total}</span>
|
|
1226
|
+
<span id="theme-label">${ui.theme} · ${escapeHtml(themeLabel)}</span>
|
|
1227
|
+
<button id="notes-toggle" type="button" title="S · ${escapeHtml(ui.notesToggle)}" aria-controls="notes-panel" aria-expanded="false">${escapeHtml(ui.notesToggle)}</button>
|
|
1228
|
+
<button id="presenter-toggle" type="button" title="V · ${escapeHtml(ui.presenterToggle)}">${escapeHtml(ui.presenterToggle)}</button>
|
|
1229
|
+
<button id="overview-toggle" type="button" title="G · ${escapeHtml(ui.overviewToggle)}" aria-pressed="false">${escapeHtml(ui.overviewToggle)}</button>
|
|
1230
|
+
<button id="help-toggle" type="button" title="?" aria-label="${escapeHtml(ui.helpTitle)}">?</button>
|
|
1231
|
+
<button id="fullscreen" type="button" title="F · Fullscreen" aria-label="Fullscreen">⛶</button>
|
|
1232
|
+
</div>
|
|
1233
|
+
<div id="notes-panel" role="region" aria-label="${escapeHtml(ui.notesLabel)}">
|
|
1234
|
+
<div class="notes-head">
|
|
1235
|
+
<span class="notes-label">${escapeHtml(ui.notesLabel)}</span>
|
|
1236
|
+
<button id="notes-close" type="button" aria-label="${escapeHtml(ui.notesCloseLabel)}">×</button>
|
|
1237
|
+
</div>
|
|
1238
|
+
<div id="notes-text"></div>
|
|
1239
|
+
</div>
|
|
1240
|
+
<div id="help-panel" hidden>
|
|
1241
|
+
<div class="notes-label">${escapeHtml(ui.helpTitle)}</div>
|
|
1242
|
+
<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>Esc</kbd> ${escapeHtml(ui.closeLabel)} · <kbd>?</kbd> ${escapeHtml(ui.helpTitle)}</p>
|
|
1243
|
+
<p>${escapeHtml(ui.presenterHint)}</p>
|
|
1244
|
+
<p>${escapeHtml(ui.overviewHint)}</p>
|
|
1245
|
+
</div>
|
|
1246
|
+
<div id="toast" role="status" hidden></div>
|
|
1247
|
+
<script>
|
|
1248
|
+
(() => {
|
|
1249
|
+
const frames = Array.from(document.querySelectorAll('.slide-frame'));
|
|
1250
|
+
const slides = Array.from(document.querySelectorAll('.slide'));
|
|
1251
|
+
const counter = document.getElementById('counter');
|
|
1252
|
+
const progress = document.getElementById('progress');
|
|
1253
|
+
const fullscreenBtn = document.getElementById('fullscreen');
|
|
1254
|
+
const notesPanel = document.getElementById('notes-panel');
|
|
1255
|
+
const notesText = document.getElementById('notes-text');
|
|
1256
|
+
const notesBtn = document.getElementById('notes-toggle');
|
|
1257
|
+
const notesClose = document.getElementById('notes-close');
|
|
1258
|
+
const presenterBtn = document.getElementById('presenter-toggle');
|
|
1259
|
+
const overviewBtn = document.getElementById('overview-toggle');
|
|
1260
|
+
const helpBtn = document.getElementById('help-toggle');
|
|
1261
|
+
const helpPanel = document.getElementById('help-panel');
|
|
1262
|
+
const toastEl = document.getElementById('toast');
|
|
1263
|
+
let index = 0;
|
|
1264
|
+
let presenterWin = null;
|
|
1265
|
+
let toastTimer = 0;
|
|
1266
|
+
const total = slides.length;
|
|
1267
|
+
const ui = ${JSON.stringify(ui).replace(/</g, '\\u003c')};
|
|
1268
|
+
|
|
1269
|
+
function notesOf(i) { return slides[i] && slides[i].dataset ? (slides[i].dataset.notes || '') : ''; }
|
|
1270
|
+
function titleOf(i) {
|
|
1271
|
+
const el = slides[i] ? slides[i].querySelector('h1,h2,.statement-title,.quote-text') : null;
|
|
1272
|
+
return el ? el.textContent.trim() : '';
|
|
1273
|
+
}
|
|
1274
|
+
function state() {
|
|
1275
|
+
const next = (index + 1) % total;
|
|
1276
|
+
return { index: index, total: total, title: titleOf(index), notes: notesOf(index), nextTitle: titleOf(next), nextNotes: notesOf(next) };
|
|
1277
|
+
}
|
|
1278
|
+
window.__dshPpt = { go: function (delta) { go(index + delta); }, jump: function (i) { go(i); }, state: state };
|
|
1279
|
+
|
|
1280
|
+
function toast(message) {
|
|
1281
|
+
toastEl.textContent = message;
|
|
1282
|
+
toastEl.hidden = false;
|
|
1283
|
+
clearTimeout(toastTimer);
|
|
1284
|
+
toastTimer = setTimeout(function () { toastEl.hidden = true; }, 2600);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function syncNotes() {
|
|
1288
|
+
const notes = notesOf(index);
|
|
1289
|
+
notesText.textContent = notes || ui.noNotes;
|
|
1290
|
+
notesText.classList.toggle('is-empty', notes === '');
|
|
1291
|
+
notesBtn.setAttribute('aria-expanded', document.body.classList.contains('notes-open') ? 'true' : 'false');
|
|
1292
|
+
updatePresenter();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
function updatePresenter() {
|
|
1296
|
+
if (presenterOpen() && typeof presenterWin.__deckPresenterUpdate === 'function') {
|
|
1297
|
+
try { presenterWin.__deckPresenterUpdate(state()); } catch (_) { /* window closing */ }
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function go(next) {
|
|
1302
|
+
const wrapped = next < 0 || next >= total;
|
|
1303
|
+
index = (next + total) % total;
|
|
1304
|
+
slides.forEach(function (slide, i) {
|
|
1305
|
+
slide.classList.toggle('is-active', i === index);
|
|
1306
|
+
if (frames[i]) frames[i].classList.toggle('is-current', i === index);
|
|
1307
|
+
});
|
|
1308
|
+
counter.textContent = ui.slide + ' ' + (index + 1) + ' ' + ui.of + ' ' + total;
|
|
1309
|
+
progress.style.width = ((index + 1) / total * 100) + '%';
|
|
1310
|
+
document.title = (index + 1) + ' / ' + total + ' · ' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')};
|
|
1311
|
+
try { history.replaceState?.(null, '', '#slide-' + (index + 1)); } catch (_) { /* file:// 下个别浏览器可能拒绝 */ }
|
|
1312
|
+
if (wrapped) toast(ui.wrapHint + ' ' + (index + 1) + ' / ' + total);
|
|
1313
|
+
syncNotes();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function notesOpen() { return document.body.classList.contains('notes-open'); }
|
|
1317
|
+
function presenterOpen() { try { return !!(presenterWin && !presenterWin.closed); } catch (_) { return false; } }
|
|
1318
|
+
|
|
1319
|
+
function setNotes(on) {
|
|
1320
|
+
document.body.classList.toggle('notes-open', on);
|
|
1321
|
+
if (on) setOverview(false);
|
|
1322
|
+
syncNotes();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function updateThumbScale() {
|
|
1326
|
+
if (!document.body.classList.contains('overview')) return;
|
|
1327
|
+
requestAnimationFrame(function () {
|
|
1328
|
+
const width = frames[0] ? frames[0].getBoundingClientRect().width : 0;
|
|
1329
|
+
if (width > 0) document.documentElement.style.setProperty('--deck-thumb-scale', String(width / window.innerWidth));
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function setOverview(on) {
|
|
1334
|
+
document.body.classList.toggle('overview', on);
|
|
1335
|
+
overviewBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1336
|
+
frames.forEach(function (frame) { frame.tabIndex = on ? 0 : -1; });
|
|
1337
|
+
if (on) {
|
|
1338
|
+
document.body.classList.remove('notes-open');
|
|
1339
|
+
helpPanel.hidden = true;
|
|
1340
|
+
syncNotes();
|
|
1341
|
+
updateThumbScale();
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function presenterHtml() {
|
|
1346
|
+
const cs = getComputedStyle(document.documentElement);
|
|
1347
|
+
const val = function (name, fallback) { const v = cs.getPropertyValue(name).trim(); return v || fallback; };
|
|
1348
|
+
const css = ':root{--bg:' + val('--bg', '#070B14') + ';--panel:' + val('--panel', '#0D1424') + ';--fg:' + val('--fg', '#E8F1FF') + ';--muted:' + val('--muted', '#7E8BA8') + ';--accent:' + val('--accent', '#7C3AED') + ';--accent2:' + val('--accent2', '#06B6D4') + ';--font-heading:' + val('--font-heading', 'sans-serif') + ';--font-body:' + val('--font-body', 'sans-serif') + '}'
|
|
1349
|
+
+ 'body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--font-body);height:100vh;display:flex;flex-direction:column}'
|
|
1350
|
+
+ 'header{display:flex;justify-content:space-between;gap:16px;padding:14px 20px;border-bottom:1px solid color-mix(in srgb, var(--muted) 40%, transparent);font-size:14px;color:var(--muted)}'
|
|
1351
|
+
+ 'header #ptitle{color:var(--fg);font-weight:700}'
|
|
1352
|
+
+ 'main{flex:1;display:grid;grid-template-columns:1.05fr 1fr;gap:18px;padding:18px 20px;min-height:0}'
|
|
1353
|
+
+ 'section{background:var(--panel);border:1px solid color-mix(in srgb, var(--muted) 30%, transparent);border-radius:14px;padding:16px 18px;min-height:0;display:flex;flex-direction:column;gap:10px}'
|
|
1354
|
+
+ '.lbl{color:var(--accent);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase}'
|
|
1355
|
+
+ '#pcur{font-family:var(--font-heading);font-size:30px;font-weight:800;line-height:1.2}'
|
|
1356
|
+
+ '#pnext{font-size:20px;line-height:1.35}'
|
|
1357
|
+
+ '#pnotes{font-size:19px;line-height:1.65;white-space:pre-wrap;overflow:auto;flex:1}'
|
|
1358
|
+
+ '.hint{color:var(--muted);font-size:12px;margin-top:auto}'
|
|
1359
|
+
+ 'footer{display:flex;align-items:center;gap:16px;padding:12px 20px;border-top:1px solid color-mix(in srgb, var(--muted) 40%, transparent)}'
|
|
1360
|
+
+ '#ptimer{font-variant-numeric:tabular-nums;font-size:22px;font-weight:700;color:var(--accent)}'
|
|
1361
|
+
+ '.bar{flex:1;height:8px;background:color-mix(in srgb, var(--muted) 28%, transparent);border-radius:99px;overflow:hidden}'
|
|
1362
|
+
+ '#pprog{height:100%;width:0;background:var(--accent);transition:width .2s}'
|
|
1363
|
+
+ 'body.big #pnotes{font-size:26px}'
|
|
1364
|
+
+ '@media (max-width:760px){main{grid-template-columns:1fr}}';
|
|
1365
|
+
const js = 'var started=Date.now(),elapsed=0,paused=false,big=false;'
|
|
1366
|
+
+ 'function fmt(ms){var s=Math.max(0,Math.floor(ms/1000));var m=Math.floor(s/60);s=s%60;return (m<10?"0":"")+m+":"+(s<10?"0":"")+s}'
|
|
1367
|
+
+ 'function tick(){if(!paused)elapsed=Date.now()-started;document.getElementById("ptimer").textContent=fmt(elapsed);requestAnimationFrame(tick)}'
|
|
1368
|
+
+ 'window.__deckPresenterUpdate=function(st){document.getElementById("pcur").textContent=st.title||("Slide "+(st.index+1));document.getElementById("pnext").textContent=st.nextTitle||"\u2014";document.getElementById("pnotes").textContent=st.notes||"' + ui.noNotes.replace(/"/g, '\\"') + '";document.getElementById("pcounter").textContent=(st.index+1)+" / "+st.total;document.getElementById("pprog").style.width=((st.index+1)/st.total*100)+"%"}'
|
|
1369
|
+
+ ';document.addEventListener("keydown",function(e){var k=e.key;'
|
|
1370
|
+
+ 'if(k==="ArrowRight"||k===" "){e.preventDefault();try{opener.__dshPpt.go(1)}catch(_){}}'
|
|
1371
|
+
+ 'else if(k==="ArrowLeft"){e.preventDefault();try{opener.__dshPpt.go(-1)}catch(_){}}'
|
|
1372
|
+
+ 'else if(k==="Escape"){window.close()}'
|
|
1373
|
+
+ 'else if(k.toLowerCase()==="t"){started=Date.now();elapsed=0;paused=false}'
|
|
1374
|
+
+ 'else if(k.toLowerCase()==="p"){paused=!paused;if(!paused)started=Date.now()-elapsed}'
|
|
1375
|
+
+ 'else if(k==="+"||k==="="){big=true;document.body.classList.add("big")}'
|
|
1376
|
+
+ 'else if(k==="-"){big=false;document.body.classList.remove("big")}});'
|
|
1377
|
+
+ 'setInterval(function(){try{if(opener&&!opener.closed&&opener.__dshPpt)window.__deckPresenterUpdate(opener.__dshPpt.state())}catch(_){}},400);'
|
|
1378
|
+
+ 'tick();';
|
|
1379
|
+
return '<!DOCTYPE html><html lang="' + ${JSON.stringify(lang.attr)} + '"><head><meta charset="UTF-8"><title>' + ${JSON.stringify(manifest.title + ' · ' + ui.presenterLabel).replace(/</g, '\\u003c')} + '</title><style>' + css + '</style></head><body>'
|
|
1380
|
+
+ '<header><div id="ptitle">' + ${JSON.stringify(manifest.title).replace(/</g, '\\u003c')} + '</div><div id="pcounter"></div></header>'
|
|
1381
|
+
+ '<main><section><div class="lbl">' + ${JSON.stringify(ui.currentLabel).replace(/</g, '\\u003c')} + '</div><div id="pcur"></div><div class="lbl">' + ${JSON.stringify(ui.nextLabel).replace(/</g, '\\u003c')} + '</div><div id="pnext"></div></section>'
|
|
1382
|
+
+ '<section><div class="lbl">' + ${JSON.stringify(ui.notesLabel).replace(/</g, '\\u003c')} + '</div><div id="pnotes"></div><div class="hint">+ / - 字号 · T 重置计时 · P 暂停 · ← → 翻页 · Esc 关闭</div></section></main>'
|
|
1383
|
+
+ '<footer><div id="ptimer">00:00</div><div class="bar"><div id="pprog"></div></div></footer>'
|
|
1384
|
+
+ '<scr' + 'ipt>' + js + '</scr' + 'ipt></body></html>';
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function openPresenter() {
|
|
1388
|
+
if (presenterOpen()) { presenterWin.focus(); return; }
|
|
1389
|
+
presenterWin = window.open('', 'dsh-ppt-presenter', 'width=1180,height=760');
|
|
1390
|
+
if (!presenterWin) { toast(ui.popupBlocked); return; }
|
|
1391
|
+
try {
|
|
1392
|
+
presenterWin.document.open();
|
|
1393
|
+
presenterWin.document.write(presenterHtml());
|
|
1394
|
+
presenterWin.document.close();
|
|
1395
|
+
} catch (_) { toast(ui.popupBlocked); return; }
|
|
1396
|
+
document.body.classList.add('presenting');
|
|
1397
|
+
if (document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur();
|
|
1398
|
+
setTimeout(updatePresenter, 150);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function closePresenter() {
|
|
1402
|
+
if (presenterOpen()) { try { presenterWin.close(); } catch (_) { /* already gone */ } }
|
|
1403
|
+
document.body.classList.remove('presenting');
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
setInterval(function () {
|
|
1407
|
+
if (!presenterOpen() && document.body.classList.contains('presenting')) document.body.classList.remove('presenting');
|
|
1408
|
+
}, 800);
|
|
1409
|
+
|
|
1410
|
+
document.addEventListener('keydown', (event) => {
|
|
1411
|
+
const key = event.key;
|
|
1412
|
+
const lower = key.toLowerCase();
|
|
1413
|
+
const targetEl = event.target;
|
|
1414
|
+
const interactive = targetEl && (targetEl.tagName === 'BUTTON' || targetEl.tagName === 'INPUT' || targetEl.tagName === 'SELECT' || targetEl.tagName === 'TEXTAREA' || targetEl.isContentEditable === true);
|
|
1415
|
+
if (key === ' ' && interactive) return;
|
|
1416
|
+
if (key === 'Escape') {
|
|
1417
|
+
if (!helpPanel.hidden) { helpPanel.hidden = true; return; }
|
|
1418
|
+
if (document.body.classList.contains('overview')) { setOverview(false); return; }
|
|
1419
|
+
if (notesOpen()) { setNotes(false); return; }
|
|
1420
|
+
if (document.fullscreenElement) { document.exitFullscreen?.(); return; }
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const inOverview = document.body.classList.contains('overview');
|
|
1424
|
+
if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || (key === 'ArrowDown' && !inOverview)) {
|
|
1425
|
+
event.preventDefault(); go(index + 1);
|
|
1426
|
+
} else if (key === 'ArrowLeft' || key === 'PageUp' || (key === 'ArrowUp' && !inOverview)) {
|
|
1427
|
+
event.preventDefault(); go(index - 1);
|
|
1428
|
+
} else if (/^[1-9]$/.test(key) && !inOverview) {
|
|
1429
|
+
const jumped = Number(key);
|
|
1430
|
+
if (jumped <= total) { event.preventDefault(); go(jumped - 1); }
|
|
1431
|
+
} else if (key === 'Home') {
|
|
1432
|
+
event.preventDefault(); go(0);
|
|
1433
|
+
} else if (key === 'End') {
|
|
1434
|
+
event.preventDefault(); go(total - 1);
|
|
1435
|
+
} else if (key === 'Enter' && inOverview) {
|
|
1436
|
+
event.preventDefault(); setOverview(false);
|
|
1437
|
+
} else if (lower === 'f') {
|
|
1438
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
|
|
1439
|
+
else document.exitFullscreen?.();
|
|
1440
|
+
} else if (lower === 'g') {
|
|
1441
|
+
setOverview(!inOverview);
|
|
1442
|
+
} else if (lower === 's') {
|
|
1443
|
+
if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); }
|
|
1444
|
+
else { document.body.classList.toggle('notes-open'); syncNotes(); }
|
|
1445
|
+
} else if (lower === 'v') {
|
|
1446
|
+
if (presenterOpen()) closePresenter();
|
|
1447
|
+
else openPresenter();
|
|
1448
|
+
} else if (lower === 'p') {
|
|
1449
|
+
window.print();
|
|
1450
|
+
} else if (key === '?') {
|
|
1451
|
+
helpPanel.hidden = !helpPanel.hidden;
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
let wheelLock = 0;
|
|
1456
|
+
document.addEventListener('wheel', (event) => {
|
|
1457
|
+
const now = Date.now();
|
|
1458
|
+
if (now - wheelLock < 550 || document.body.classList.contains('overview') || notesOpen()) return;
|
|
1459
|
+
wheelLock = now;
|
|
1460
|
+
if (Math.abs(event.deltaY) > 12) go(index + (event.deltaY > 0 ? 1 : -1));
|
|
1461
|
+
}, { passive: true });
|
|
1462
|
+
|
|
1463
|
+
let touchStartY = 0;
|
|
1464
|
+
document.addEventListener('touchstart', (event) => { touchStartY = event.touches[0].clientY; }, { passive: true });
|
|
1465
|
+
document.addEventListener('touchend', (event) => {
|
|
1466
|
+
const delta = event.changedTouches[0].clientY - touchStartY;
|
|
1467
|
+
if (Math.abs(delta) > 48 && !document.body.classList.contains('overview')) go(index + (delta < 0 ? 1 : -1));
|
|
1468
|
+
}, { passive: true });
|
|
1469
|
+
|
|
1470
|
+
fullscreenBtn.addEventListener('click', () => {
|
|
1471
|
+
if (!document.fullscreenElement) document.documentElement.requestFullscreen?.();
|
|
1472
|
+
else document.exitFullscreen?.();
|
|
1473
|
+
});
|
|
1474
|
+
notesBtn.addEventListener('click', () => {
|
|
1475
|
+
if (document.body.classList.contains('presenting')) { toast(ui.presenterHint); return; }
|
|
1476
|
+
setNotes(!notesOpen());
|
|
1477
|
+
});
|
|
1478
|
+
notesClose.addEventListener('click', () => setNotes(false));
|
|
1479
|
+
presenterBtn.addEventListener('click', () => { if (presenterOpen()) closePresenter(); else openPresenter(); });
|
|
1480
|
+
overviewBtn.addEventListener('click', () => setOverview(!document.body.classList.contains('overview')));
|
|
1481
|
+
helpBtn.addEventListener('click', () => { helpPanel.hidden = !helpPanel.hidden; });
|
|
1482
|
+
frames.forEach((frame, i) => {
|
|
1483
|
+
frame.addEventListener('click', () => {
|
|
1484
|
+
if (document.body.classList.contains('overview')) { go(i); setOverview(false); }
|
|
1485
|
+
});
|
|
1486
|
+
frame.addEventListener('keydown', (event) => {
|
|
1487
|
+
if (event.key === 'Enter' && document.body.classList.contains('overview')) { go(i); setOverview(false); }
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
window.addEventListener('resize', updateThumbScale);
|
|
1491
|
+
window.addEventListener('beforeunload', closePresenter);
|
|
1492
|
+
|
|
1493
|
+
const start = Number.parseInt(location.hash?.replace('#slide-', ''), 10);
|
|
1494
|
+
go(Number.isInteger(start) ? start - 1 : 0);
|
|
1495
|
+
})();
|
|
1496
|
+
</script>
|
|
1497
|
+
</body>
|
|
1498
|
+
</html>`
|
|
1499
|
+
}
|
|
1086
1500
|
function renderHtmlSlide(slide, index, ui, langId) {
|
|
1087
1501
|
const kicker = slide.kicker || (slide.layout === 'cover' ? ui.coverKicker : '')
|
|
1088
1502
|
const title = slide.title || ''
|
|
@@ -1118,12 +1532,15 @@ function renderHtmlSlide(slide, index, ui, langId) {
|
|
|
1118
1532
|
const rows = Array.isArray(slide.rows) ? slide.rows : []
|
|
1119
1533
|
const head = rows[0] ?? []
|
|
1120
1534
|
const body = rows.slice(1)
|
|
1121
|
-
|
|
1122
|
-
|
|
1535
|
+
const isNum = (cell) => /^\s*[+-]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?\s*(?:%|%|次|页|个|人|元|天|小时|分钟)?\s*$/.test(String(cell ?? ''))
|
|
1536
|
+
const headingText = title !== '' ? title : (kicker || ui.tableKicker)
|
|
1537
|
+
const showKicker = kicker !== '' && kicker !== headingText
|
|
1538
|
+
inner = (showKicker ? '<div class="kicker">' + escapeHtml(kicker) + '</div>' : '') +
|
|
1539
|
+
'<h2>' + escapeHtml(headingText) + '</h2>' +
|
|
1123
1540
|
'<table class="deck-table"><thead><tr>' +
|
|
1124
|
-
head.map((cell) => '<th>' + escapeHtml(cell) + '</th>').join('') +
|
|
1541
|
+
head.map((cell) => '<th' + (isNum(cell) ? ' class="num"' : '') + '>' + escapeHtml(cell) + '</th>').join('') +
|
|
1125
1542
|
'</tr></thead><tbody>' +
|
|
1126
|
-
body.map((row) => '<tr>' + row.map((cell) => '<td>' + escapeHtml(cell) + '</td>').join('') + '</tr>').join('') +
|
|
1543
|
+
body.map((row) => '<tr>' + row.map((cell) => '<td' + (isNum(cell) ? ' class="num"' : '') + '>' + escapeHtml(cell) + '</td>').join('') + '</tr>').join('') +
|
|
1127
1544
|
'</tbody></table>'
|
|
1128
1545
|
break
|
|
1129
1546
|
}
|