dsh-ppt 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.en.md +9 -4
- package/README.md +10 -5
- package/lib/config.js +23 -1
- package/lib/execution.d.ts +18 -0
- package/lib/execution.js +56 -0
- package/lib/index.d.ts +4 -14
- package/lib/index.js +5 -159
- package/lib/tools.d.ts +17 -0
- package/lib/tools.js +123 -0
- package/lib/types.d.ts +10 -2
- package/package.json +70 -70
- package/skills/dsh-ppt/SKILL.md +31 -73
- package/skills/dsh-ppt/references/copywriting.md +3 -12
- package/skills/dsh-ppt/references/syntax.md +44 -0
- package/skills/dsh-ppt/references/themes.md +4 -0
- package/skills/dsh-ppt/references/troubleshooting.md +13 -0
- package/skills/dsh-ppt/scripts/build-deck.mjs +8 -2
- package/skills/dsh-ppt/scripts/deck-core.mjs +90 -10
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* 零运行时依赖,仅使用 node:fs / node:path / node:zlib。
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { resolve as resolvePath } from 'node:path'
|
|
19
19
|
import { deflateRawSync } from 'node:zlib'
|
|
20
20
|
|
|
21
|
-
export const DECK_VERSION = '0.
|
|
21
|
+
export const DECK_VERSION = '0.3.0'
|
|
22
22
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// 主题
|
|
@@ -206,6 +206,13 @@ export function resolveLanguage(input) {
|
|
|
206
206
|
return language
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export function resolveMotion(input) {
|
|
210
|
+
if (input === undefined || input === null || input === '') return true
|
|
211
|
+
if (input === true || input === 'on') return true
|
|
212
|
+
if (input === false || input === 'off') return false
|
|
213
|
+
throw new Error('dsh-ppt:未知 motion 值 "' + String(input) + '",可选:on / off(默认 on)')
|
|
214
|
+
}
|
|
215
|
+
|
|
209
216
|
export function listThemes(lang = 'zh') {
|
|
210
217
|
const pick = (pair) => (lang === 'en' ? pair.en : pair.zh)
|
|
211
218
|
return THEME_IDS.map((id) => {
|
|
@@ -242,6 +249,22 @@ export function sanitizeFileName(input) {
|
|
|
242
249
|
return cleaned.slice(0, 120) || 'deck'
|
|
243
250
|
}
|
|
244
251
|
|
|
252
|
+
const DECK_ARTIFACT_EXTENSIONS = ['.html', '.pptx', '.json']
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 为三件套选择同一个可用文件名前缀。默认不覆盖:任一产物已存在时,
|
|
256
|
+
* 整组改用 -1/-2… 后缀,避免新旧 deck 被静默混写。
|
|
257
|
+
*/
|
|
258
|
+
function resolveDeckFileName(outputDir, requestedFileName, overwrite) {
|
|
259
|
+
const isAvailable = (candidate) => DECK_ARTIFACT_EXTENSIONS.every((ext) => !existsSync(resolvePath(outputDir, candidate + ext)))
|
|
260
|
+
if (overwrite || isAvailable(requestedFileName)) return requestedFileName
|
|
261
|
+
for (let index = 1; index < 1000; index += 1) {
|
|
262
|
+
const candidate = requestedFileName + '-' + index
|
|
263
|
+
if (isAvailable(candidate)) return candidate
|
|
264
|
+
}
|
|
265
|
+
throw new Error('dsh-ppt:找不到可用的输出文件名(同名前缀超过 999 个),请更换 fileName 或显式允许 overwrite。')
|
|
266
|
+
}
|
|
267
|
+
|
|
245
268
|
export function escapeXml(value) {
|
|
246
269
|
return String(value ?? '')
|
|
247
270
|
.replace(/&/g, '&')
|
|
@@ -626,6 +649,7 @@ export function normalizeBuildOptions(options = {}) {
|
|
|
626
649
|
if (title === '') throw new Error('dsh-ppt:title 不能为空')
|
|
627
650
|
const theme = resolveTheme(options.theme)
|
|
628
651
|
const language = resolveLanguage(options.lang)
|
|
652
|
+
const motion = resolveMotion(options.motion)
|
|
629
653
|
const maxSlides = clampInt(options.maxSlides, 60, 3, 120)
|
|
630
654
|
let deck
|
|
631
655
|
if (Array.isArray(options.slides) && options.slides.length > 0) {
|
|
@@ -646,19 +670,22 @@ export function normalizeBuildOptions(options = {}) {
|
|
|
646
670
|
if (deck.slides.length < 1) throw new Error('dsh-ppt:没有可生成的幻灯片')
|
|
647
671
|
const outputDir = resolvePath(String(options.outputDir ?? '.').trim() || '.')
|
|
648
672
|
const fileName = sanitizeFileName(options.fileName ?? title)
|
|
649
|
-
|
|
673
|
+
const overwrite = options.overwrite === true
|
|
674
|
+
return { title, theme, language, motion, deck, outputDir, fileName, overwrite }
|
|
650
675
|
}
|
|
651
676
|
|
|
652
677
|
export function buildDeck(options = {}) {
|
|
653
678
|
const normalized = normalizeBuildOptions(options)
|
|
654
|
-
const { title, theme, language, deck, outputDir,
|
|
679
|
+
const { title, theme, language, motion, deck, outputDir, overwrite } = normalized
|
|
655
680
|
mkdirSync(outputDir, { recursive: true })
|
|
681
|
+
const fileName = resolveDeckFileName(outputDir, normalized.fileName, overwrite)
|
|
656
682
|
|
|
657
683
|
const manifest = {
|
|
658
684
|
version: DECK_VERSION,
|
|
659
685
|
title,
|
|
660
686
|
theme: theme.id,
|
|
661
687
|
language: language.id,
|
|
688
|
+
motion,
|
|
662
689
|
slideCount: deck.slides.length,
|
|
663
690
|
slides: deck.slides,
|
|
664
691
|
}
|
|
@@ -696,6 +723,16 @@ export function renderHtml(manifest, theme, language) {
|
|
|
696
723
|
const t = theme
|
|
697
724
|
const lang = language
|
|
698
725
|
const ui = lang.ui
|
|
726
|
+
const motion = manifest.motion !== false
|
|
727
|
+
const motionCss = motion
|
|
728
|
+
? `body.motion .slide.is-active{animation:slide-in .45s cubic-bezier(.22,.8,.36,1)}
|
|
729
|
+
@keyframes slide-in{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:none}}
|
|
730
|
+
body.motion .bullets li{
|
|
731
|
+
opacity:0;animation:bullet-in .55s cubic-bezier(.22,.8,.36,1) forwards;
|
|
732
|
+
animation-delay:calc(var(--i, 0)*90ms + .12s);
|
|
733
|
+
}
|
|
734
|
+
@keyframes bullet-in{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}`
|
|
735
|
+
: ''
|
|
699
736
|
const slides = manifest.slides.map((slide, index) => renderHtmlSlide(slide, index, ui, lang.id)).join('\n')
|
|
700
737
|
const themeLabel = t.name[lang.id] ?? t.name.en
|
|
701
738
|
const total = manifest.slides.length
|
|
@@ -735,8 +772,8 @@ body{
|
|
|
735
772
|
radial-gradient(36% 30% at 12% 86%, ${hexToRgba(t.palette.accent2, t.dark ? 0.16 : 0.12)}, transparent 70%),
|
|
736
773
|
radial-gradient(70% 60% at 50% 50%, ${hexToRgba(t.palette.panel, 0.55)}, transparent 100%);
|
|
737
774
|
}
|
|
738
|
-
.slide.is-active{display:flex
|
|
739
|
-
|
|
775
|
+
.slide.is-active{display:flex}
|
|
776
|
+
${motionCss}
|
|
740
777
|
.kicker{
|
|
741
778
|
color:var(--accent);font-weight:700;letter-spacing:.18em;text-transform:uppercase;
|
|
742
779
|
font-size:clamp(12px,1.3vw,18px);margin-bottom:22px;
|
|
@@ -826,11 +863,12 @@ body.overview #notes-panel{display:none !important}
|
|
|
826
863
|
@media print{
|
|
827
864
|
html,body{height:auto;overflow:visible;background:#fff}
|
|
828
865
|
.slide{position:relative;display:block !important;height:100vh;page-break-after:always;padding:48px}
|
|
866
|
+
${motion ? 'body.motion .bullets li{opacity:1 !important;animation:none !important}' : ''}
|
|
829
867
|
#progress,#hud,#notes-panel{display:none !important}
|
|
830
868
|
}
|
|
831
869
|
</style>
|
|
832
870
|
</head>
|
|
833
|
-
<body>
|
|
871
|
+
<body class="${motion ? 'motion' : 'no-motion'}">
|
|
834
872
|
<div id="progress" aria-hidden="true"></div>
|
|
835
873
|
${slides}
|
|
836
874
|
<div id="hud" aria-live="polite">
|
|
@@ -979,7 +1017,7 @@ function renderHtmlSlide(slide, index, ui, langId) {
|
|
|
979
1017
|
default:
|
|
980
1018
|
inner = '<div class="kicker">' + escapeHtml(kicker) + '</div>' +
|
|
981
1019
|
'<h2>' + escapeHtml(title) + '</h2>' +
|
|
982
|
-
'<ul>' + bullets.map((bullet) => '<li>' + escapeHtml(bullet) + '</li>').join('') + '</ul>'
|
|
1020
|
+
'<ul>' + bullets.map((bullet, bulletIndex) => '<li style="--i:' + bulletIndex + '">' + escapeHtml(bullet) + '</li>').join('') + '</ul>'
|
|
983
1021
|
break
|
|
984
1022
|
}
|
|
985
1023
|
const notesAttr = typeof slide.notes === 'string' && slide.notes.trim() !== ''
|
|
@@ -1209,8 +1247,48 @@ function slideShapeList(slide, index, total, theme, langId) {
|
|
|
1209
1247
|
return shapes.join('')
|
|
1210
1248
|
}
|
|
1211
1249
|
|
|
1212
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* 原生逐条点击显现动画(PowerPoint「出现,按段落」的标准时序树):
|
|
1252
|
+
* bldP 把正文文本框标记为按段落构建,主序列里每条要点一个点击节点。
|
|
1253
|
+
*/
|
|
1254
|
+
function bulletTimingXml(spid, clicks) {
|
|
1255
|
+
let id = 2
|
|
1256
|
+
const nextId = () => { id += 1; return String(id) }
|
|
1257
|
+
const clickPars = []
|
|
1258
|
+
for (let i = 0; i < clicks; i += 1) {
|
|
1259
|
+
clickPars.push(
|
|
1260
|
+
'<p:par><p:cTn id="' + nextId() + '" fill="hold"><p:stCondLst><p:cond delay="indefinite"/></p:stCondLst><p:childTnLst>' +
|
|
1261
|
+
'<p:par><p:cTn id="' + nextId() + '" presetID="1" presetClass="entr" presetSubtype="0" fill="hold" grpId="0" nodeType="clickEffect">' +
|
|
1262
|
+
'<p:stCondLst><p:cond delay="0"/></p:stCondLst><p:childTnLst>' +
|
|
1263
|
+
'<p:set><p:cBhvr><p:cTn id="' + nextId() + '" dur="1" fill="hold"><p:stCondLst><p:cond delay="0"/></p:stCondLst></p:cTn>' +
|
|
1264
|
+
'<p:tgtEl><p:spTgt spid="' + spid + '"/></p:tgtEl>' +
|
|
1265
|
+
'<p:attrNameLst><p:attrName>style.visibility</p:attrName></p:attrNameLst></p:cBhvr>' +
|
|
1266
|
+
'<p:to><p:strVal val="visible"/></p:to></p:set>' +
|
|
1267
|
+
'</p:childTnLst></p:cTn></p:par>' +
|
|
1268
|
+
'</p:childTnLst></p:cTn></p:par>',
|
|
1269
|
+
)
|
|
1270
|
+
}
|
|
1271
|
+
return '<p:timing><p:tnLst><p:par>' +
|
|
1272
|
+
'<p:cTn id="1" dur="indefinite" restart="never" nodeType="tmRoot"><p:childTnLst>' +
|
|
1273
|
+
'<p:seq concurrent="1" nextAc="seek"><p:cTn id="2" dur="indefinite" nodeType="mainSeq"><p:childTnLst>' +
|
|
1274
|
+
clickPars.join('') +
|
|
1275
|
+
'</p:childTnLst></p:cTn>' +
|
|
1276
|
+
'<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>' +
|
|
1277
|
+
'<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>' +
|
|
1278
|
+
'</p:seq></p:childTnLst></p:cTn></p:par></p:tnLst>' +
|
|
1279
|
+
'<p:bldLst><p:bldP spid="' + spid + '" grpId="0"/></p:bldLst></p:timing>'
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function slideXml(slide, index, total, theme, langId, motion) {
|
|
1213
1283
|
const p = theme.palette
|
|
1284
|
+
let motionXml = ''
|
|
1285
|
+
if (motion) {
|
|
1286
|
+
motionXml = '<p:transition spd="med"><p:fade/></p:transition>'
|
|
1287
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets : []
|
|
1288
|
+
if (slide.layout === 'bullets' && bullets.length >= 2) {
|
|
1289
|
+
motionXml += bulletTimingXml((index + 1) * 10 + 3, bullets.length)
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1214
1292
|
return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
|
|
1215
1293
|
'<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" ' +
|
|
1216
1294
|
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
|
|
@@ -1221,6 +1299,7 @@ function slideXml(slide, index, total, theme, langId) {
|
|
|
1221
1299
|
slideShapeList(slide, index, total, theme, langId) +
|
|
1222
1300
|
'</p:spTree></p:cSld>' +
|
|
1223
1301
|
'<p:clrMapOvr><a:overrideClrMapping bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/></p:clrMapOvr>' +
|
|
1302
|
+
motionXml +
|
|
1224
1303
|
'</p:sld>'
|
|
1225
1304
|
}
|
|
1226
1305
|
|
|
@@ -1530,6 +1609,7 @@ function crc32(buffer) {
|
|
|
1530
1609
|
export function buildPptx(manifest, themeInput, languageInput) {
|
|
1531
1610
|
const theme = themeInput?.id ? themeInput : resolveTheme(themeInput)
|
|
1532
1611
|
const language = languageInput?.id ? languageInput : resolveLanguage(languageInput)
|
|
1612
|
+
const motion = manifest.motion !== false
|
|
1533
1613
|
const slides = manifest.slides
|
|
1534
1614
|
const noteIndexes = []
|
|
1535
1615
|
slides.forEach((slide, index) => {
|
|
@@ -1555,7 +1635,7 @@ export function buildPptx(manifest, themeInput, languageInput) {
|
|
|
1555
1635
|
}
|
|
1556
1636
|
slides.forEach((slide, index) => {
|
|
1557
1637
|
const hasNotes = noteIndexes.includes(index)
|
|
1558
|
-
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.id) })
|
|
1638
|
+
entries.push({ name: 'ppt/slides/slide' + (index + 1) + '.xml', data: slideXml(slide, index, slides.length, theme, language.id, motion) })
|
|
1559
1639
|
entries.push({ name: 'ppt/slides/_rels/slide' + (index + 1) + '.xml.rels', data: slideRelXml(hasNotes, index + 1) })
|
|
1560
1640
|
if (hasNotes) {
|
|
1561
1641
|
entries.push({ name: 'ppt/notesSlides/notesSlide' + (index + 1) + '.xml', data: notesSlideXml(slide.notes) })
|