@christophervr/pptx-viewer 1.5.9 → 1.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/dist/cli.mjs ADDED
@@ -0,0 +1,1641 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/colors.ts
4
+ var isColorEnabled = process.env.NO_COLOR === void 0 && (process.env.FORCE_COLOR !== void 0 || Boolean(process.stdout.isTTY));
5
+ function wrap(open, close) {
6
+ return (text) => isColorEnabled ? `\x1B[${open}m${text}\x1B[${close}m` : text;
7
+ }
8
+ var bold = wrap(1, 22);
9
+ var dim = wrap(2, 22);
10
+ var red = wrap(31, 39);
11
+ var green = wrap(32, 39);
12
+ var yellow = wrap(33, 39);
13
+ var blue = wrap(34, 39);
14
+ var magenta = wrap(35, 39);
15
+ var cyan = wrap(36, 39);
16
+ var gray = wrap(90, 39);
17
+ function isUnicodeSupported() {
18
+ if (process.platform !== "win32") {
19
+ return true;
20
+ }
21
+ return Boolean(process.env.CI) || Boolean(process.env.WT_SESSION) || Boolean(process.env.ConEmuTask) || process.env.TERM_PROGRAM === "vscode" || process.env.TERM === "xterm-256color";
22
+ }
23
+ var symbols = isUnicodeSupported() ? { pointer: "\u276F", check: "\u2714", cross: "\u2718", radioOn: "\u25C9", radioOff: "\u25EF", bullet: "\xB7" } : { pointer: ">", check: "\u221A", cross: "\xD7", radioOn: "(*)", radioOff: "( )", bullet: "*" };
24
+
25
+ // src/orchestrate.ts
26
+ import { existsSync as existsSync4 } from "fs";
27
+
28
+ // src/args.ts
29
+ var KNOWN_PMS = ["bun", "pnpm", "yarn", "npm"];
30
+ function readFlagValue(args, index, flag) {
31
+ const value = args[index + 1];
32
+ if (!value) {
33
+ throw new Error(`${flag} needs a value`);
34
+ }
35
+ return value;
36
+ }
37
+ function parseArgs(args) {
38
+ const parsed = { help: false, yes: false, scaffold: false };
39
+ for (let i = 0; i < args.length; i++) {
40
+ const arg = args[i];
41
+ switch (arg) {
42
+ case "--help":
43
+ case "-h":
44
+ parsed.help = true;
45
+ break;
46
+ case "--yes":
47
+ case "-y":
48
+ parsed.yes = true;
49
+ break;
50
+ case "--scaffold":
51
+ parsed.scaffold = true;
52
+ break;
53
+ case "--target":
54
+ parsed.target = readFlagValue(args, i, arg);
55
+ i++;
56
+ break;
57
+ case "--dir":
58
+ parsed.dir = readFlagValue(args, i, arg);
59
+ i++;
60
+ break;
61
+ case "--pm": {
62
+ const value = readFlagValue(args, i, arg);
63
+ if (!KNOWN_PMS.includes(value)) {
64
+ throw new Error(`--pm must be one of: ${KNOWN_PMS.join(", ")}`);
65
+ }
66
+ parsed.pm = value;
67
+ i++;
68
+ break;
69
+ }
70
+ default:
71
+ throw new Error(`Unknown option: ${arg}`);
72
+ }
73
+ }
74
+ return parsed;
75
+ }
76
+
77
+ // src/project-deps.ts
78
+ import { existsSync, readFileSync } from "fs";
79
+ import { join } from "path";
80
+ function readJson(path) {
81
+ if (!existsSync(path)) {
82
+ return null;
83
+ }
84
+ try {
85
+ return JSON.parse(readFileSync(path, "utf8"));
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ function findInstalledVersion(cwd, pkgName) {
91
+ const resolved = readJson(join(cwd, "node_modules", pkgName, "package.json"));
92
+ if (resolved?.version) {
93
+ return { version: resolved.version, source: "resolved" };
94
+ }
95
+ const projectPkg = readJson(join(cwd, "package.json"));
96
+ if (!projectPkg) {
97
+ return null;
98
+ }
99
+ const declared = projectPkg.dependencies?.[pkgName] ?? projectPkg.devDependencies?.[pkgName] ?? projectPkg.peerDependencies?.[pkgName];
100
+ return declared ? { version: declared, source: "declared" } : null;
101
+ }
102
+
103
+ // src/semver.ts
104
+ function extractMajor(version) {
105
+ const match = /(?<major>\d+)\.\d+\.\d+/u.exec(version);
106
+ if (!match?.groups) {
107
+ return null;
108
+ }
109
+ return Number.parseInt(match.groups.major, 10);
110
+ }
111
+
112
+ // src/compat.ts
113
+ function checkCompat(cwd, target) {
114
+ if (!target.compat) {
115
+ return { compatible: true, message: null };
116
+ }
117
+ const installed = findInstalledVersion(cwd, target.compat.peerPackage);
118
+ if (!installed) {
119
+ return { compatible: true, message: null };
120
+ }
121
+ const major = extractMajor(installed.version);
122
+ if (major === null || target.compat.requiredMajors.includes(major)) {
123
+ return { compatible: true, message: null };
124
+ }
125
+ const sourceLabel = installed.source === "resolved" ? "installed" : "declared in package.json";
126
+ const supported = target.compat.requiredMajors.map((m) => `^${m}`).join(" or ");
127
+ return {
128
+ compatible: false,
129
+ message: `Detected ${target.compat.peerPackage}@${installed.version} (${sourceLabel}) in this project, but ${target.label} requires ${target.compat.peerPackage}@${supported}. Continuing may change your ${target.compat.peerPackage} version.`
130
+ };
131
+ }
132
+
133
+ // src/package-manager.ts
134
+ import { existsSync as existsSync2 } from "fs";
135
+ import { join as join2 } from "path";
136
+ var LOCKFILES = {
137
+ "bun.lock": "bun",
138
+ "bun.lockb": "bun",
139
+ "pnpm-lock.yaml": "pnpm",
140
+ "yarn.lock": "yarn",
141
+ "package-lock.json": "npm"
142
+ };
143
+ function detectPackageManager(cwd) {
144
+ for (const [file, pm] of Object.entries(LOCKFILES)) {
145
+ if (existsSync2(join2(cwd, file))) {
146
+ return pm;
147
+ }
148
+ }
149
+ const userAgent = process.env.npm_config_user_agent ?? "";
150
+ if (userAgent.startsWith("bun")) {
151
+ return "bun";
152
+ }
153
+ if (userAgent.startsWith("pnpm")) {
154
+ return "pnpm";
155
+ }
156
+ if (userAgent.startsWith("yarn")) {
157
+ return "yarn";
158
+ }
159
+ return "npm";
160
+ }
161
+ function installCommand(pm, packages) {
162
+ switch (pm) {
163
+ case "bun":
164
+ return ["bun", ["add", ...packages]];
165
+ case "pnpm":
166
+ return ["pnpm", ["add", ...packages]];
167
+ case "yarn":
168
+ return ["yarn", ["add", ...packages]];
169
+ case "npm":
170
+ return ["npm", ["install", ...packages]];
171
+ }
172
+ }
173
+
174
+ // src/prompt.ts
175
+ import { createInterface } from "readline/promises";
176
+
177
+ // src/interactive-menu.ts
178
+ import { emitKeypressEvents } from "readline";
179
+ var HIDE_CURSOR = "\x1B[?25l";
180
+ var SHOW_CURSOR = "\x1B[?25h";
181
+ var CLEAR_LINE = "\x1B[2K";
182
+ var ERASE_DOWN = "\x1B[0J";
183
+ function moveUp(lines) {
184
+ return lines > 0 ? `\x1B[${lines}A` : "";
185
+ }
186
+ function isEnterKey(str, key) {
187
+ return key?.name === "return" || key?.name === "enter" || str === "\r" || str === "\n";
188
+ }
189
+ function renderChoice(choice, isCursor, checked) {
190
+ const pointer = isCursor ? cyan(symbols.pointer) : " ";
191
+ const box = checked === null ? "" : `${checked ? green(symbols.radioOn) : gray(symbols.radioOff)} `;
192
+ const label = isCursor ? bold(choice.label) : choice.label;
193
+ return `${pointer} ${box}${label} ${dim(`- ${choice.description}`)}`;
194
+ }
195
+ function groupMatesOf(choices, index) {
196
+ const group = choices[index].group;
197
+ if (!group) {
198
+ return [];
199
+ }
200
+ return choices.flatMap((c, i) => i !== index && c.group === group ? [i] : []);
201
+ }
202
+ function runMenu(choices, multi) {
203
+ return new Promise((resolve) => {
204
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
205
+ resolve(null);
206
+ return;
207
+ }
208
+ let cursor = 0;
209
+ const checked = /* @__PURE__ */ new Set();
210
+ let statusMessage = "";
211
+ let settled = false;
212
+ const hint = multi ? dim("(\u2191/\u2193 move, space toggle, a select all, enter confirm)") : dim("(\u2191/\u2193 move, enter confirm)");
213
+ const totalLines = choices.length + 2;
214
+ process.stdout.write(HIDE_CURSOR);
215
+ function draw(first) {
216
+ if (!first) {
217
+ process.stdout.write(moveUp(totalLines));
218
+ }
219
+ process.stdout.write(`${CLEAR_LINE}${hint}
220
+ `);
221
+ for (const [i, choice] of choices.entries()) {
222
+ const checkedState = multi ? checked.has(i) : null;
223
+ process.stdout.write(`${CLEAR_LINE}${renderChoice(choice, i === cursor, checkedState)}
224
+ `);
225
+ }
226
+ process.stdout.write(`${CLEAR_LINE}${statusMessage}
227
+ `);
228
+ }
229
+ function eraseWidget() {
230
+ process.stdout.write(moveUp(totalLines));
231
+ process.stdout.write(ERASE_DOWN);
232
+ }
233
+ function cleanup() {
234
+ process.stdin.setRawMode?.(false);
235
+ process.stdin.removeListener("keypress", onKeypress);
236
+ process.stdin.pause();
237
+ eraseWidget();
238
+ process.stdout.write(SHOW_CURSOR);
239
+ }
240
+ function finish(result) {
241
+ if (settled) {
242
+ return;
243
+ }
244
+ settled = true;
245
+ cleanup();
246
+ resolve(result);
247
+ }
248
+ function check(index) {
249
+ for (const mate of groupMatesOf(choices, index)) {
250
+ checked.delete(mate);
251
+ }
252
+ checked.add(index);
253
+ }
254
+ function toggleSelectAll() {
255
+ const selectable = choices.flatMap((c, i) => c.group ? [] : [i]);
256
+ const allSelected = selectable.every((i) => checked.has(i));
257
+ for (const i of selectable) {
258
+ if (allSelected) {
259
+ checked.delete(i);
260
+ } else {
261
+ checked.add(i);
262
+ }
263
+ }
264
+ }
265
+ function onKeypress(str, key) {
266
+ if (key?.ctrl && key.name === "c") {
267
+ finish(null);
268
+ process.exit(130);
269
+ return;
270
+ }
271
+ statusMessage = "";
272
+ if (key?.name === "up") {
273
+ cursor = (cursor - 1 + choices.length) % choices.length;
274
+ draw(false);
275
+ } else if (key?.name === "down") {
276
+ cursor = (cursor + 1) % choices.length;
277
+ draw(false);
278
+ } else if (multi && key?.name === "space") {
279
+ if (checked.has(cursor)) {
280
+ checked.delete(cursor);
281
+ } else {
282
+ check(cursor);
283
+ }
284
+ draw(false);
285
+ } else if (multi && key?.name === "a") {
286
+ toggleSelectAll();
287
+ draw(false);
288
+ } else if (isEnterKey(str, key)) {
289
+ if (multi) {
290
+ if (checked.size > 0) {
291
+ finish([...checked].sort((a, b) => a - b));
292
+ } else {
293
+ statusMessage = dim("Select at least one option with space, then press enter.");
294
+ draw(false);
295
+ }
296
+ } else {
297
+ finish([cursor]);
298
+ }
299
+ }
300
+ }
301
+ emitKeypressEvents(process.stdin);
302
+ process.stdin.setRawMode(true);
303
+ process.stdin.on("keypress", onKeypress);
304
+ process.stdin.resume();
305
+ draw(true);
306
+ });
307
+ }
308
+
309
+ // src/prompt.ts
310
+ function parseSelection(answer, count) {
311
+ const trimmed = answer.trim().toLowerCase();
312
+ if (trimmed === "all" || trimmed === "a") {
313
+ return Array.from({ length: count }, (_, i) => i);
314
+ }
315
+ const tokens = trimmed.split(/[,\s]+/u).filter(Boolean);
316
+ if (tokens.length === 0) {
317
+ return null;
318
+ }
319
+ const indices = /* @__PURE__ */ new Set();
320
+ for (const token of tokens) {
321
+ const n = Number.parseInt(token, 10);
322
+ if (!Number.isInteger(n) || n < 1 || n > count) {
323
+ return null;
324
+ }
325
+ indices.add(n - 1);
326
+ }
327
+ return [...indices].sort((a, b) => a - b);
328
+ }
329
+ function printOptions(options) {
330
+ options.forEach((opt, i) => {
331
+ console.log(` ${cyan(`${i + 1})`)} ${bold(opt.label)} ${dim(`- ${opt.description}`)}`);
332
+ });
333
+ }
334
+ async function selectByNumber(options) {
335
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
336
+ try {
337
+ printOptions(options);
338
+ for (; ; ) {
339
+ const answer = (await rl.question(`
340
+ Enter a number (1-${options.length}): `)).trim();
341
+ const index = Number.parseInt(answer, 10) - 1;
342
+ if (Number.isInteger(index) && index >= 0 && index < options.length) {
343
+ return options[index];
344
+ }
345
+ console.log(`Please enter a number between 1 and ${options.length}.`);
346
+ }
347
+ } finally {
348
+ rl.close();
349
+ }
350
+ }
351
+ async function selectManyByNumber(options) {
352
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
353
+ try {
354
+ printOptions(options);
355
+ for (; ; ) {
356
+ const answer = await rl.question(
357
+ `
358
+ Enter one or more numbers, comma-separated (e.g. "1,3"), or "all": `
359
+ );
360
+ const indices = parseSelection(answer, options.length);
361
+ if (indices && indices.length > 0) {
362
+ return indices.map((i) => options[i]);
363
+ }
364
+ console.log(`Please enter at least one number between 1 and ${options.length}, or "all".`);
365
+ }
366
+ } finally {
367
+ rl.close();
368
+ }
369
+ }
370
+ async function selectOption(question, options) {
371
+ console.log(`
372
+ ${bold(question)}`);
373
+ const picked = await runMenu(options, false);
374
+ if (!picked) {
375
+ return selectByNumber(options);
376
+ }
377
+ const choice = options[picked[0]];
378
+ console.log(`${green("\u2714")} ${choice.label}`);
379
+ return choice;
380
+ }
381
+ async function multiSelect(question, options) {
382
+ console.log(`
383
+ ${bold(question)}`);
384
+ const picked = await runMenu(options, true);
385
+ if (!picked) {
386
+ return selectManyByNumber(options);
387
+ }
388
+ const choices = picked.map((i) => options[i]);
389
+ console.log(`${green("\u2714")} ${choices.map((c) => c.label).join(", ")}`);
390
+ return choices;
391
+ }
392
+ async function confirm(question) {
393
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
394
+ try {
395
+ const answer = (await rl.question(`${bold(question)} ${dim("(Y/n)")} `)).trim().toLowerCase();
396
+ return answer === "" || answer === "y" || answer === "yes";
397
+ } finally {
398
+ rl.close();
399
+ }
400
+ }
401
+ async function input(question, defaultValue) {
402
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
403
+ try {
404
+ const answer = (await rl.question(`${bold(question)} ${dim(`(${defaultValue})`)} `)).trim();
405
+ return answer === "" ? defaultValue : answer;
406
+ } finally {
407
+ rl.close();
408
+ }
409
+ }
410
+
411
+ // src/templates/angular.ts
412
+ var ANGULAR_APP_TS = `import { Component, signal } from '@angular/core';
413
+ import { PptxHandler } from 'pptx-viewer-core';
414
+ import type { CollaborationConfig } from 'pptx-angular-viewer';
415
+ import { PowerPointViewerComponent } from 'pptx-angular-viewer';
416
+
417
+ /**
418
+ * The presentation formats this viewer can open: OOXML and the legacy binary
419
+ * PowerPoint format, which pptx-viewer-core converts on load. Kept as an
420
+ * explicit check because a drop event carries no accept filtering.
421
+ */
422
+ function isPresentation(file: File | undefined): file is File {
423
+ const name = file?.name.toLowerCase() ?? '';
424
+ return name.endsWith('.pptx') || name.endsWith('.ppt');
425
+ }
426
+
427
+ @Component({
428
+ selector: 'app-root',
429
+ standalone: true,
430
+ imports: [PowerPointViewerComponent],
431
+ styles: [\`
432
+ :host { display: block; height: 100dvh; }
433
+ .stage { display: flex; align-items: center; justify-content: center; height: 100dvh; padding: 2rem; cursor: default; }
434
+ .dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem; max-width: 520px; width: 100%; padding: 3rem; text-align: center; border: 2px dashed var(--pptx-border, #374151); border-radius: 0.75rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
435
+ .dropzone.over, .dropzone:hover { border-color: var(--pptx-primary, #6366f1); background: var(--pptx-muted, rgba(255,255,255,0.04)); }
436
+ h1 { margin: 0; font-size: 1.5rem; font-weight: 500; }
437
+ p { margin: 0; font-size: 0.875rem; color: var(--pptx-muted-foreground, #9ca3af); }
438
+ .pick-label { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: 1px solid var(--pptx-border, #374151); background: var(--pptx-muted, #1f2937); color: var(--pptx-foreground, #f3f4f6); cursor: pointer; font-size: 0.875rem; transition: background 0.15s; }
439
+ .pick-label:hover { background: var(--pptx-accent, #374151); }
440
+ .or-sep { font-size: 0.8rem; color: var(--pptx-muted-foreground, #6b7280); }
441
+ .new-btn { padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: none; background: var(--pptx-primary, #6366f1); color: #fff; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: opacity 0.15s; }
442
+ .new-btn:hover { opacity: 0.9; }
443
+ \`],
444
+ template: \`
445
+ @if (content(); as c) {
446
+ <div style="height: 100dvh">
447
+ <pptx-power-point-viewer
448
+ [content]="c"
449
+ [canEdit]="true"
450
+ style="height: 100%"
451
+ [collaboration]="collab()"
452
+ (startCollaboration)="collab.set($event)"
453
+ (stopCollaboration)="collab.set(undefined)"
454
+ />
455
+ </div>
456
+ } @else {
457
+ <div
458
+ class="stage"
459
+ [class.over]="over()"
460
+ (dragover)="$event.preventDefault(); over.set(true)"
461
+ (dragleave)="over.set(false)"
462
+ (drop)="onDrop($event)"
463
+ (click)="fileInput.click()"
464
+ >
465
+ <div class="dropzone">
466
+ <h1>Open a Presentation</h1>
467
+ <p>Drag &amp; drop a .pptx or .ppt file here, or</p>
468
+ <label class="pick-label" (click)="$event.stopPropagation()">
469
+ Choose a file
470
+ <input #fileInput type="file" accept=".pptx,.ppt" style="display: none" (change)="onPick($event)" />
471
+ </label>
472
+ <span class="or-sep">or</span>
473
+ <button class="new-btn" (click)="$event.stopPropagation(); newPresentation()">New Presentation</button>
474
+ </div>
475
+ </div>
476
+ }
477
+ \`,
478
+ })
479
+ export class App {
480
+ content = signal<ArrayBuffer | Uint8Array | null>(null);
481
+ collab = signal<CollaborationConfig | undefined>(undefined);
482
+ over = signal(false);
483
+
484
+ async onDrop(e: DragEvent) {
485
+ e.preventDefault();
486
+ this.over.set(false);
487
+ const file = e.dataTransfer?.files?.[0];
488
+ if (isPresentation(file)) this.content.set(await file.arrayBuffer());
489
+ }
490
+
491
+ async onPick(e: Event) {
492
+ const file = (e.target as HTMLInputElement).files?.[0];
493
+ if (file) this.content.set(await file.arrayBuffer());
494
+ }
495
+
496
+ async newPresentation() {
497
+ const { handler, data } = await PptxHandler.createBlank({
498
+ title: 'Untitled Presentation',
499
+ initialSlideCount: 1,
500
+ });
501
+ this.content.set(await handler.save(data.slides));
502
+ }
503
+ }
504
+ `;
505
+ var ANGULAR_MAIN_TS = `import 'zone.js';
506
+ import '@angular/compiler';
507
+ import { bootstrapApplication } from '@angular/platform-browser';
508
+ import { Injectable } from '@angular/core';
509
+ import type { MissingTranslationHandlerParams } from '@ngx-translate/core';
510
+ import { MissingTranslationHandler, provideTranslateService } from '@ngx-translate/core';
511
+ import { keyToLabel } from 'pptx-angular-viewer';
512
+
513
+ import { App } from './app/app.ts';
514
+
515
+ @Injectable()
516
+ class LabelFallbackHandler implements MissingTranslationHandler {
517
+ handle(params: MissingTranslationHandlerParams): string {
518
+ return keyToLabel(params.key);
519
+ }
520
+ }
521
+
522
+ bootstrapApplication(App, {
523
+ providers: [
524
+ provideTranslateService({
525
+ lang: 'en',
526
+ fallbackLang: 'en',
527
+ missingTranslationHandler: {
528
+ provide: MissingTranslationHandler,
529
+ useClass: LabelFallbackHandler,
530
+ },
531
+ }),
532
+ ],
533
+ }).catch((err) => console.error(err));
534
+ `;
535
+
536
+ // src/templates/react.ts
537
+ var REACT_APP_TSX = `import { useCallback, useState } from 'react';
538
+ import { PptxHandler } from 'pptx-viewer-core';
539
+ import type { CollaborationConfig } from 'pptx-react-viewer';
540
+ import { PowerPointViewer } from 'pptx-react-viewer';
541
+ import 'pptx-react-viewer/styles.css';
542
+ import './i18n';
543
+
544
+ /**
545
+ * The presentation formats this viewer can open: OOXML and the legacy binary
546
+ * PowerPoint format, which pptx-viewer-core converts on load. Kept as an
547
+ * explicit check because a drop event carries no accept filtering.
548
+ */
549
+ function isPresentation(file: File | undefined): file is File {
550
+ const name = file?.name.toLowerCase() ?? '';
551
+ return name.endsWith('.pptx') || name.endsWith('.ppt');
552
+ }
553
+
554
+ export default function App() {
555
+ const [content, setContent] = useState<Uint8Array | null>(null);
556
+ const [over, setOver] = useState(false);
557
+ const [collab, setCollab] = useState<CollaborationConfig | undefined>();
558
+
559
+ const loadFile = useCallback(async (file: File) => {
560
+ setContent(new Uint8Array(await file.arrayBuffer()));
561
+ }, []);
562
+
563
+ const newPresentation = useCallback(async () => {
564
+ const { handler, data } = await PptxHandler.createBlank({
565
+ title: 'Untitled Presentation',
566
+ initialSlideCount: 1,
567
+ });
568
+ setContent(await handler.save(data.slides));
569
+ }, []);
570
+
571
+ if (content) {
572
+ return (
573
+ <div style={{ height: '100dvh' }}>
574
+ <PowerPointViewer
575
+ content={content}
576
+ canEdit
577
+ collaboration={collab}
578
+ onStartCollaboration={setCollab}
579
+ onStopCollaboration={() => setCollab(undefined)}
580
+ />
581
+ </div>
582
+ );
583
+ }
584
+
585
+ return (
586
+ <div className="stage">
587
+ <div
588
+ className={\`dropzone\${over ? ' over' : ''}\`}
589
+ onDragOver={(e) => { e.preventDefault(); setOver(true); }}
590
+ onDragLeave={() => setOver(false)}
591
+ onDrop={(e) => {
592
+ e.preventDefault();
593
+ setOver(false);
594
+ const file = e.dataTransfer.files[0];
595
+ if (isPresentation(file)) void loadFile(file);
596
+ }}
597
+ onClick={() => document.getElementById('file-input')?.click()}
598
+ >
599
+ <h1>Open a Presentation</h1>
600
+ <p>Drag &amp; drop a .pptx or .ppt file here, or</p>
601
+ <label className="pick-label" onClick={(e) => e.stopPropagation()}>
602
+ Choose a file
603
+ <input
604
+ id="file-input"
605
+ type="file"
606
+ accept=".pptx,.ppt"
607
+ style={{ display: 'none' }}
608
+ onChange={(e) => {
609
+ const file = e.target.files?.[0];
610
+ if (file) void loadFile(file);
611
+ }}
612
+ />
613
+ </label>
614
+ <span className="or-sep">or</span>
615
+ <button
616
+ className="new-btn"
617
+ onClick={(e) => { e.stopPropagation(); void newPresentation(); }}
618
+ >
619
+ New Presentation
620
+ </button>
621
+ </div>
622
+ </div>
623
+ );
624
+ }
625
+ `;
626
+ var REACT_I18N_TS = `import { createInstance } from 'i18next';
627
+ import { translationsEn, keyToLabel } from 'pptx-react-viewer/i18n';
628
+ import { initReactI18next } from 'react-i18next';
629
+
630
+ const i18n = createInstance();
631
+
632
+ i18n.use(initReactI18next).init({
633
+ resources: {
634
+ en: { translation: translationsEn },
635
+ },
636
+ lng: 'en',
637
+ fallbackLng: 'en',
638
+ interpolation: { escapeValue: false },
639
+ parseMissingKeyHandler: (key: string) => keyToLabel(key),
640
+ missingKeyHandler: false,
641
+ });
642
+
643
+ export default i18n;
644
+ `;
645
+
646
+ // src/templates/shared.ts
647
+ var MINIMAL_APP_CSS = `:root {
648
+ color-scheme: dark;
649
+ }
650
+
651
+ *,
652
+ *::before,
653
+ *::after {
654
+ box-sizing: border-box;
655
+ }
656
+
657
+ body {
658
+ margin: 0;
659
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
660
+ overflow-x: hidden;
661
+ background: var(--pptx-background, #030712);
662
+ color: var(--pptx-foreground, #f3f4f6);
663
+ }
664
+
665
+ #app,
666
+ #root {
667
+ height: 100dvh;
668
+ }
669
+
670
+ .stage {
671
+ display: flex;
672
+ align-items: center;
673
+ justify-content: center;
674
+ height: 100dvh;
675
+ padding: 2rem;
676
+ }
677
+
678
+ .dropzone {
679
+ display: flex;
680
+ flex-direction: column;
681
+ align-items: center;
682
+ justify-content: center;
683
+ gap: 0.75rem;
684
+ max-width: 520px;
685
+ width: 100%;
686
+ padding: 3rem;
687
+ text-align: center;
688
+ border: 2px dashed var(--pptx-border, #374151);
689
+ border-radius: 0.75rem;
690
+ cursor: pointer;
691
+ transition:
692
+ border-color 0.15s,
693
+ background 0.15s;
694
+ }
695
+
696
+ .dropzone.over,
697
+ .dropzone:hover {
698
+ border-color: var(--pptx-primary, #6366f1);
699
+ background: var(--pptx-muted, rgba(255, 255, 255, 0.04));
700
+ }
701
+
702
+ .dropzone h1 {
703
+ margin: 0;
704
+ font-size: 1.5rem;
705
+ font-weight: 500;
706
+ }
707
+
708
+ .dropzone p {
709
+ margin: 0;
710
+ font-size: 0.875rem;
711
+ color: var(--pptx-muted-foreground, #9ca3af);
712
+ }
713
+
714
+ .pick-label {
715
+ display: inline-flex;
716
+ align-items: center;
717
+ gap: 0.5rem;
718
+ padding: 0.5rem 1.25rem;
719
+ border-radius: 0.5rem;
720
+ border: 1px solid var(--pptx-border, #374151);
721
+ background: var(--pptx-muted, #1f2937);
722
+ color: var(--pptx-foreground, #f3f4f6);
723
+ cursor: pointer;
724
+ font-size: 0.875rem;
725
+ transition: background 0.15s;
726
+ }
727
+
728
+ .pick-label:hover {
729
+ background: var(--pptx-accent, #374151);
730
+ }
731
+
732
+ .or-sep {
733
+ font-size: 0.8rem;
734
+ color: var(--pptx-muted-foreground, #6b7280);
735
+ }
736
+
737
+ .new-btn {
738
+ padding: 0.5rem 1.25rem;
739
+ border-radius: 0.5rem;
740
+ border: none;
741
+ background: var(--pptx-primary, #6366f1);
742
+ color: #fff;
743
+ cursor: pointer;
744
+ font-size: 0.875rem;
745
+ font-weight: 500;
746
+ transition: opacity 0.15s;
747
+ }
748
+
749
+ .new-btn:hover {
750
+ opacity: 0.9;
751
+ }
752
+ `;
753
+ var ANGULAR_GLOBAL_CSS = `:root {
754
+ color-scheme: dark;
755
+ }
756
+
757
+ *,
758
+ *::before,
759
+ *::after {
760
+ box-sizing: border-box;
761
+ }
762
+
763
+ body {
764
+ margin: 0;
765
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
766
+ background: var(--pptx-background, #030712);
767
+ color: var(--pptx-foreground, #f3f4f6);
768
+ }
769
+
770
+ app-root {
771
+ display: block;
772
+ height: 100dvh;
773
+ }
774
+ `;
775
+
776
+ // src/templates/svelte.ts
777
+ var SVELTE_APP_SVELTE = `<script lang="ts">
778
+ import { PptxHandler } from 'pptx-viewer-core';
779
+ import type { CollaborationConfig } from 'pptx-svelte-viewer';
780
+ import { PowerPointViewer } from 'pptx-svelte-viewer';
781
+
782
+ /**
783
+ * The presentation formats this viewer can open: OOXML and the legacy binary
784
+ * PowerPoint format, which pptx-viewer-core converts on load. Kept as an
785
+ * explicit check because a drop event carries no accept filtering.
786
+ */
787
+ function isPresentation(file: File | undefined): file is File {
788
+ const name = file?.name.toLowerCase() ?? '';
789
+ return name.endsWith('.pptx') || name.endsWith('.ppt');
790
+ }
791
+
792
+ let content = $state<Uint8Array | null>(null);
793
+ let over = $state(false);
794
+ let collab = $state<CollaborationConfig | undefined>();
795
+
796
+ async function loadFile(file: File) {
797
+ content = new Uint8Array(await file.arrayBuffer());
798
+ }
799
+
800
+ function onDrop(e: DragEvent) {
801
+ over = false;
802
+ const file = e.dataTransfer?.files?.[0];
803
+ if (isPresentation(file)) void loadFile(file);
804
+ }
805
+
806
+ function onPick(e: Event) {
807
+ const file = (e.target as HTMLInputElement).files?.[0];
808
+ if (file) void loadFile(file);
809
+ }
810
+
811
+ async function newPresentation() {
812
+ const { handler, data } = await PptxHandler.createBlank({
813
+ title: 'Untitled Presentation',
814
+ initialSlideCount: 1,
815
+ });
816
+ content = await handler.save(data.slides);
817
+ }
818
+ </script>
819
+
820
+ {#if content}
821
+ <div style="height: 100dvh">
822
+ <PowerPointViewer
823
+ source={content}
824
+ editable
825
+ collaboration={collab}
826
+ onstartcollaboration={(cfg) => { collab = cfg; }}
827
+ onstopcollaboration={() => { collab = undefined; }}
828
+ />
829
+ </div>
830
+ {:else}
831
+ <div
832
+ class="stage"
833
+ ondragover={(e) => { e.preventDefault(); over = true; }}
834
+ ondragleave={() => { over = false; }}
835
+ ondrop={(e) => { e.preventDefault(); onDrop(e); }}
836
+ onclick={() => document.getElementById('file-input')?.click()}
837
+ role="button"
838
+ tabindex="0"
839
+ >
840
+ <div class="dropzone" class:over>
841
+ <h1>Open a Presentation</h1>
842
+ <p>Drag &amp; drop a .pptx or .ppt file here, or</p>
843
+ <label class="pick-label" onclick={(e) => e.stopPropagation()}>
844
+ Choose a file
845
+ <input id="file-input" type="file" accept=".pptx,.ppt" style="display: none" onchange={onPick} />
846
+ </label>
847
+ <span class="or-sep">or</span>
848
+ <button class="new-btn" onclick={(e) => { e.stopPropagation(); void newPresentation(); }}>
849
+ New Presentation
850
+ </button>
851
+ </div>
852
+ </div>
853
+ {/if}
854
+ `;
855
+
856
+ // src/templates/vanilla.ts
857
+ var VANILLA_MAIN_TS = `import { createPptxViewer } from 'pptx-vanilla-viewer';
858
+ import { PptxHandler } from 'pptx-viewer-core';
859
+
860
+ import './style.css';
861
+
862
+ /**
863
+ * The presentation formats this viewer can open: OOXML and the legacy binary
864
+ * PowerPoint format, which pptx-viewer-core converts on load. Kept as an
865
+ * explicit check because a drop event carries no accept filtering.
866
+ */
867
+ function isPresentation(file: File | undefined): file is File {
868
+ const name = file?.name.toLowerCase() ?? '';
869
+ return name.endsWith('.pptx') || name.endsWith('.ppt');
870
+ }
871
+
872
+ const app = document.querySelector<HTMLDivElement>('#app')!;
873
+
874
+ function show(source: ArrayBuffer | Uint8Array): void {
875
+ app.innerHTML = '';
876
+ app.style.height = '100dvh';
877
+ createPptxViewer(app, { source, editable: true });
878
+ }
879
+
880
+ function showLanding(): void {
881
+ app.style.height = '';
882
+ app.innerHTML = '';
883
+
884
+ const stage = document.createElement('div');
885
+ stage.className = 'stage';
886
+
887
+ const zone = document.createElement('div');
888
+ zone.className = 'dropzone';
889
+
890
+ const h1 = document.createElement('h1');
891
+ h1.textContent = 'Open a Presentation';
892
+
893
+ const hint = document.createElement('p');
894
+ hint.textContent = 'Drag & drop a .pptx or .ppt file here, or';
895
+
896
+ const label = document.createElement('label');
897
+ label.className = 'pick-label';
898
+ label.textContent = 'Choose a file';
899
+
900
+ const input = document.createElement('input');
901
+ input.type = 'file';
902
+ input.accept = '.pptx,.ppt';
903
+ input.style.display = 'none';
904
+ label.append(input);
905
+
906
+ const orSep = document.createElement('span');
907
+ orSep.className = 'or-sep';
908
+ orSep.textContent = 'or';
909
+
910
+ const newBtn = document.createElement('button');
911
+ newBtn.className = 'new-btn';
912
+ newBtn.textContent = 'New Presentation';
913
+
914
+ zone.append(h1, hint, label, orSep, newBtn);
915
+ stage.append(zone);
916
+ app.append(stage);
917
+
918
+ zone.addEventListener('dragover', (e) => {
919
+ e.preventDefault();
920
+ zone.classList.add('over');
921
+ });
922
+ zone.addEventListener('dragleave', () => zone.classList.remove('over'));
923
+ zone.addEventListener('drop', (e) => {
924
+ e.preventDefault();
925
+ zone.classList.remove('over');
926
+ const file = e.dataTransfer?.files?.[0];
927
+ if (isPresentation(file)) void file.arrayBuffer().then(show);
928
+ });
929
+
930
+ // Click the zone to open the file picker (but not if the button was clicked).
931
+ zone.addEventListener('click', () => input.click());
932
+ label.addEventListener('click', (e) => e.stopPropagation());
933
+ input.addEventListener('click', (e) => e.stopPropagation());
934
+ input.addEventListener('change', () => {
935
+ const file = input.files?.[0];
936
+ if (file) void file.arrayBuffer().then(show);
937
+ });
938
+
939
+ newBtn.addEventListener('click', async (e) => {
940
+ e.stopPropagation();
941
+ newBtn.textContent = 'Creating...';
942
+ newBtn.disabled = true;
943
+ const { handler, data } = await PptxHandler.createBlank({
944
+ title: 'Untitled Presentation',
945
+ initialSlideCount: 1,
946
+ });
947
+ show(await handler.save(data.slides));
948
+ });
949
+ }
950
+
951
+ showLanding();
952
+ `;
953
+
954
+ // src/templates/vue.ts
955
+ var VUE_APP_VUE = `<script setup lang="ts">
956
+ import { ref } from 'vue';
957
+ import { PptxHandler } from 'pptx-viewer-core';
958
+ import type { CollaborationConfig } from 'pptx-vue-viewer';
959
+ import { PowerPointViewer } from 'pptx-vue-viewer';
960
+ import 'pptx-vue-viewer/styles.css';
961
+
962
+ /**
963
+ * The presentation formats this viewer can open: OOXML and the legacy binary
964
+ * PowerPoint format, which pptx-viewer-core converts on load. Kept as an
965
+ * explicit check because a drop event carries no accept filtering.
966
+ */
967
+ function isPresentation(file: File | undefined): file is File {
968
+ const name = file?.name.toLowerCase() ?? '';
969
+ return name.endsWith('.pptx') || name.endsWith('.ppt');
970
+ }
971
+
972
+ const content = ref<Uint8Array>();
973
+ const over = ref(false);
974
+ const collab = ref<CollaborationConfig | undefined>();
975
+
976
+ async function loadFile(file: File) {
977
+ content.value = new Uint8Array(await file.arrayBuffer());
978
+ }
979
+
980
+ function onDrop(e: DragEvent) {
981
+ over.value = false;
982
+ const file = e.dataTransfer?.files?.[0];
983
+ if (isPresentation(file)) void loadFile(file);
984
+ }
985
+
986
+ function onPick(e: Event) {
987
+ const file = (e.target as HTMLInputElement).files?.[0];
988
+ if (file) void loadFile(file);
989
+ }
990
+
991
+ async function newPresentation() {
992
+ const { handler, data } = await PptxHandler.createBlank({
993
+ title: 'Untitled Presentation',
994
+ initialSlideCount: 1,
995
+ });
996
+ content.value = await handler.save(data.slides);
997
+ }
998
+ </script>
999
+
1000
+ <template>
1001
+ <div v-if="content" style="height: 100dvh">
1002
+ <PowerPointViewer
1003
+ :content="content"
1004
+ can-edit
1005
+ style="height: 100%"
1006
+ :collaboration="collab"
1007
+ @start-collaboration="collab = $event"
1008
+ @stop-collaboration="collab = undefined"
1009
+ />
1010
+ </div>
1011
+ <div
1012
+ v-else
1013
+ class="stage"
1014
+ @dragover.prevent="over = true"
1015
+ @dragleave="over = false"
1016
+ @drop.prevent="onDrop($event as DragEvent)"
1017
+ @click="($refs.input as HTMLInputElement).click()"
1018
+ >
1019
+ <div :class="['dropzone', { over }]">
1020
+ <h1>Open a Presentation</h1>
1021
+ <p>Drag &amp; drop a .pptx or .ppt file here, or</p>
1022
+ <label class="pick-label" @click.stop>
1023
+ Choose a file
1024
+ <input ref="input" type="file" accept=".pptx,.ppt" style="display: none" @change="onPick" />
1025
+ </label>
1026
+ <span class="or-sep">or</span>
1027
+ <button class="new-btn" @click.stop="newPresentation">New Presentation</button>
1028
+ </div>
1029
+ </div>
1030
+ </template>
1031
+
1032
+ <style>
1033
+ :root { color-scheme: dark; }
1034
+ *, *::before, *::after { box-sizing: border-box; }
1035
+ body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--pptx-background, #030712); color: var(--pptx-foreground, #f3f4f6); }
1036
+ #app { height: 100dvh; }
1037
+ .stage { display: flex; align-items: center; justify-content: center; height: 100dvh; padding: 2rem; }
1038
+ .dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem; max-width: 520px; width: 100%; padding: 3rem; text-align: center; border: 2px dashed var(--pptx-border, #374151); border-radius: 0.75rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
1039
+ .dropzone.over, .dropzone:hover { border-color: var(--pptx-primary, #6366f1); background: var(--pptx-muted, rgba(255, 255, 255, 0.04)); }
1040
+ .dropzone h1 { margin: 0; font-size: 1.5rem; font-weight: 500; }
1041
+ .dropzone p { margin: 0; font-size: 0.875rem; color: var(--pptx-muted-foreground, #9ca3af); }
1042
+ .pick-label { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: 1px solid var(--pptx-border, #374151); background: var(--pptx-muted, #1f2937); color: var(--pptx-foreground, #f3f4f6); cursor: pointer; font-size: 0.875rem; transition: background 0.15s; }
1043
+ .pick-label:hover { background: var(--pptx-accent, #374151); }
1044
+ .or-sep { font-size: 0.8rem; color: var(--pptx-muted-foreground, #6b7280); }
1045
+ .new-btn { padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: none; background: var(--pptx-primary, #6366f1); color: #fff; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: opacity 0.15s; }
1046
+ .new-btn:hover { opacity: 0.9; }
1047
+ </style>
1048
+ `;
1049
+ var VUE_MAIN_TS = `import { createApp } from 'vue';
1050
+ import { createI18n } from 'vue-i18n';
1051
+ import { translationsEn, keyToLabel } from 'pptx-vue-viewer/i18n';
1052
+ import App from './App.vue';
1053
+
1054
+ const i18n = createI18n({
1055
+ legacy: false,
1056
+ locale: 'en',
1057
+ fallbackLocale: 'en',
1058
+ messages: { en: translationsEn },
1059
+ missing: (_locale, key) => keyToLabel(key),
1060
+ missingWarn: false,
1061
+ fallbackWarn: false,
1062
+ });
1063
+
1064
+ createApp(App).use(i18n).mount('#app');
1065
+ `;
1066
+
1067
+ // src/targets.ts
1068
+ var COLLAB_EXTRAS = [
1069
+ {
1070
+ prompt: "Include real-time collaboration? (adds yjs, y-websocket, y-webrtc)",
1071
+ packages: ["yjs", "y-websocket", "y-webrtc"]
1072
+ // defaultInclude is true (the default) - demo apps ship with collab packages.
1073
+ }
1074
+ ];
1075
+ var TARGETS = [
1076
+ {
1077
+ id: "react",
1078
+ label: "React",
1079
+ description: "pptx-react-viewer - viewer/editor component for a React 18/19 app",
1080
+ mode: "install",
1081
+ group: "framework",
1082
+ packages: [
1083
+ "pptx-react-viewer",
1084
+ "react",
1085
+ "react-dom",
1086
+ "framer-motion",
1087
+ "lucide-react",
1088
+ "react-icons",
1089
+ "jspdf",
1090
+ "jszip",
1091
+ "fast-xml-parser",
1092
+ "i18next",
1093
+ "react-i18next"
1094
+ ],
1095
+ nextSteps: `import { PowerPointViewer } from 'pptx-react-viewer';
1096
+ import 'pptx-react-viewer/styles.css';
1097
+
1098
+ <PowerPointViewer content={arrayBuffer} canEdit />
1099
+
1100
+ Docs: https://www.npmjs.com/package/pptx-react-viewer`,
1101
+ compat: { peerPackage: "react", requiredMajors: [18, 19] },
1102
+ scaffold: {
1103
+ command: "create-vite@latest",
1104
+ // --no-interactive/--no-immediate stop create-vite from prompting for a linter
1105
+ // choice and then auto-installing + auto-starting its own dev server; if it did,
1106
+ // that dev server would block forever and our own entry-file patch + extra
1107
+ // package install below would never run, leaving the default Vite template in place.
1108
+ args: (dir) => [dir, "--template", "react-ts", "--no-interactive", "--no-immediate"],
1109
+ extraPackages: [
1110
+ "pptx-react-viewer",
1111
+ "pptx-viewer-core",
1112
+ "framer-motion",
1113
+ "lucide-react",
1114
+ "react-icons",
1115
+ "jspdf",
1116
+ "jszip",
1117
+ "fast-xml-parser",
1118
+ "i18next",
1119
+ "react-i18next"
1120
+ ],
1121
+ entryCandidates: ["src/App.tsx"],
1122
+ entryContent: REACT_APP_TSX,
1123
+ extraFiles: {
1124
+ "src/i18n.ts": REACT_I18N_TS,
1125
+ "src/index.css": MINIMAL_APP_CSS
1126
+ },
1127
+ optionalExtras: COLLAB_EXTRAS
1128
+ }
1129
+ },
1130
+ {
1131
+ id: "vue",
1132
+ label: "Vue",
1133
+ description: "pptx-vue-viewer - viewer/editor component for a Vue 3.5+ app",
1134
+ mode: "install",
1135
+ group: "framework",
1136
+ packages: ["pptx-vue-viewer", "vue", "jszip", "fast-xml-parser"],
1137
+ nextSteps: `<script setup lang="ts">
1138
+ import { PowerPointViewer } from 'pptx-vue-viewer';
1139
+ import 'pptx-vue-viewer/styles.css';
1140
+ </script>
1141
+
1142
+ <template>
1143
+ <PowerPointViewer :content="content" style="height: 100vh" />
1144
+ </template>
1145
+
1146
+ Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
1147
+ compat: { peerPackage: "vue", requiredMajors: [3] },
1148
+ scaffold: {
1149
+ command: "create-vite@latest",
1150
+ args: (dir) => [dir, "--template", "vue-ts", "--no-interactive", "--no-immediate"],
1151
+ extraPackages: [
1152
+ "pptx-vue-viewer",
1153
+ "pptx-viewer-core",
1154
+ "vue-i18n",
1155
+ "jszip",
1156
+ "fast-xml-parser"
1157
+ ],
1158
+ entryCandidates: ["src/App.vue"],
1159
+ entryContent: VUE_APP_VUE,
1160
+ extraFiles: { "src/main.ts": VUE_MAIN_TS },
1161
+ optionalExtras: COLLAB_EXTRAS
1162
+ }
1163
+ },
1164
+ {
1165
+ id: "angular",
1166
+ label: "Angular",
1167
+ description: "pptx-angular-viewer - viewer/editor component for an Angular 19-22 app",
1168
+ mode: "install",
1169
+ group: "framework",
1170
+ packages: ["pptx-angular-viewer", "@angular/core", "@angular/common", "rxjs"],
1171
+ nextSteps: `import { PowerPointViewerComponent } from 'pptx-angular-viewer';
1172
+ import 'pptx-angular-viewer/styles.css';
1173
+
1174
+ <pptx-power-point-viewer [content]="content" />
1175
+
1176
+ Docs: https://www.npmjs.com/package/pptx-angular-viewer`,
1177
+ compat: { peerPackage: "@angular/core", requiredMajors: [19, 20, 21, 22] },
1178
+ scaffold: {
1179
+ command: "@angular/cli@latest",
1180
+ // --no-interactive matters even with the flags above supplied: the
1181
+ // `application` schematic's `ssr` option has an `x-prompt`, and `ng new`
1182
+ // prompts for it (plus anything else not already given a value) whenever
1183
+ // stdin is a TTY, which ours is (we inherit the real user's terminal).
1184
+ args: (dir) => [
1185
+ "new",
1186
+ dir,
1187
+ "--standalone",
1188
+ "--skip-git",
1189
+ "--style=css",
1190
+ "--skip-install",
1191
+ "--no-interactive"
1192
+ ],
1193
+ extraPackages: ["pptx-angular-viewer", "pptx-viewer-core", "@ngx-translate/core"],
1194
+ // Angular v20+ generates `app.ts`; older schematics generate `app.component.ts`.
1195
+ entryCandidates: ["src/app/app.ts", "src/app/app.component.ts"],
1196
+ entryContent: ANGULAR_APP_TS,
1197
+ extraFiles: { "src/main.ts": ANGULAR_MAIN_TS, "src/styles.css": ANGULAR_GLOBAL_CSS },
1198
+ // @angular/cli@22 requires Node.js >=22.22.0, >=24.13.1, or >=26.0.0. Check
1199
+ // BEFORE the project-name prompt so the user sees the real reason immediately.
1200
+ preflight: () => {
1201
+ const node = process.versions.node;
1202
+ const [maj, min, pat] = node.split(".").map(Number);
1203
+ const ok = maj === 22 && min >= 22 || maj === 24 && (min > 13 || min === 13 && pat >= 1) || maj >= 26;
1204
+ if (!ok) {
1205
+ throw new Error(
1206
+ `@angular/cli@latest requires Node.js v22.22.0+, v24.13.1+, or v26.0.0+.
1207
+ You are running Node.js v${node}.
1208
+ Update Node.js at: https://nodejs.org`
1209
+ );
1210
+ }
1211
+ },
1212
+ optionalExtras: COLLAB_EXTRAS
1213
+ }
1214
+ },
1215
+ {
1216
+ id: "svelte",
1217
+ label: "Svelte",
1218
+ description: "pptx-svelte-viewer - viewer/editor component for a Svelte 5 app",
1219
+ mode: "install",
1220
+ group: "framework",
1221
+ packages: ["pptx-svelte-viewer", "svelte", "jszip", "fast-xml-parser"],
1222
+ // The Svelte binding compiles its styles into the components, so there
1223
+ // is no `/styles.css` subpath to import.
1224
+ nextSteps: `<script lang="ts">
1225
+ import { PowerPointViewer } from 'pptx-svelte-viewer';
1226
+ </script>
1227
+
1228
+ <PowerPointViewer source={bytes} editable />
1229
+
1230
+ Docs: https://www.npmjs.com/package/pptx-svelte-viewer`,
1231
+ compat: { peerPackage: "svelte", requiredMajors: [5] },
1232
+ scaffold: {
1233
+ command: "create-vite@latest",
1234
+ args: (dir) => [dir, "--template", "svelte-ts", "--no-interactive", "--no-immediate"],
1235
+ extraPackages: ["pptx-svelte-viewer", "pptx-viewer-core", "jszip", "fast-xml-parser"],
1236
+ entryCandidates: ["src/App.svelte"],
1237
+ entryContent: SVELTE_APP_SVELTE,
1238
+ // The starter's main.ts imports ./app.css; replace the Vite demo
1239
+ // styles (centred #app with padding) with a full-viewport reset.
1240
+ extraFiles: { "src/app.css": MINIMAL_APP_CSS },
1241
+ optionalExtras: COLLAB_EXTRAS
1242
+ }
1243
+ },
1244
+ {
1245
+ id: "vanilla",
1246
+ label: "Vanilla JS",
1247
+ description: "pptx-vanilla-viewer - zero-framework viewer/editor, plain DOM, no framework at all",
1248
+ mode: "install",
1249
+ group: "framework",
1250
+ // The vanilla binding injects its own stylesheet at runtime; jszip and
1251
+ // fast-xml-parser are its only peers.
1252
+ packages: ["pptx-vanilla-viewer", "jszip", "fast-xml-parser"],
1253
+ nextSteps: `import { createPptxViewer } from 'pptx-vanilla-viewer';
1254
+
1255
+ const viewer = createPptxViewer(document.getElementById('host')!, {
1256
+ source: '/deck.pptx', // URL, ArrayBuffer, Uint8Array, Blob, or File
1257
+ editable: true,
1258
+ });
1259
+
1260
+ Docs: https://www.npmjs.com/package/pptx-vanilla-viewer`,
1261
+ scaffold: {
1262
+ command: "create-vite@latest",
1263
+ args: (dir) => [dir, "--template", "vanilla-ts", "--no-interactive", "--no-immediate"],
1264
+ extraPackages: [
1265
+ "pptx-vanilla-viewer",
1266
+ "pptx-viewer-core",
1267
+ "three",
1268
+ "jszip",
1269
+ "fast-xml-parser"
1270
+ ],
1271
+ entryCandidates: ["src/main.ts"],
1272
+ entryContent: VANILLA_MAIN_TS,
1273
+ // main.ts imports ./style.css; replace the Vite demo styles with a
1274
+ // full-viewport reset.
1275
+ extraFiles: { "src/style.css": MINIMAL_APP_CSS },
1276
+ optionalExtras: COLLAB_EXTRAS
1277
+ }
1278
+ },
1279
+ {
1280
+ id: "core",
1281
+ label: "Core engine only",
1282
+ description: "pptx-viewer-core - framework-agnostic parse/edit/save/convert SDK, no UI",
1283
+ mode: "install",
1284
+ // jszip and fast-xml-parser are regular dependencies of pptx-viewer-core,
1285
+ // so npm/yarn/pnpm/bun pull them in automatically. Nothing else to add.
1286
+ packages: ["pptx-viewer-core"],
1287
+ nextSteps: `import { PptxHandler } from 'pptx-viewer-core';
1288
+
1289
+ const handler = new PptxHandler();
1290
+ const data = await handler.load(arrayBuffer);
1291
+ const bytes = await handler.save(data.slides);
1292
+
1293
+ Docs: https://www.npmjs.com/package/pptx-viewer-core`
1294
+ },
1295
+ {
1296
+ id: "mcp",
1297
+ label: "MCP server",
1298
+ description: "pptx-viewer-mcp - PowerPoint editing tools for AI agents (Claude, Cursor, ...)",
1299
+ mode: "print-config",
1300
+ packages: ["pptx-viewer-mcp"],
1301
+ nextSteps: `Add this to your MCP client config (Claude Desktop, Claude Code, Cursor, ...):
1302
+
1303
+ {
1304
+ "mcpServers": {
1305
+ "pptx": {
1306
+ "command": "npx",
1307
+ "args": ["pptx-viewer-mcp"]
1308
+ }
1309
+ }
1310
+ }
1311
+
1312
+ npx downloads pptx-viewer-mcp (and its bundled pptx-viewer-core engine) the
1313
+ first time your MCP client starts it, so there is nothing to install by hand.
1314
+
1315
+ Docs: https://www.npmjs.com/package/pptx-viewer-mcp`
1316
+ }
1317
+ ];
1318
+
1319
+ // src/resolve.ts
1320
+ function parseTargetIds(csv) {
1321
+ return [
1322
+ ...new Set(
1323
+ csv.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
1324
+ )
1325
+ ];
1326
+ }
1327
+ function findTargetsByIds(ids) {
1328
+ return ids.map((id) => {
1329
+ const match = TARGETS.find((t) => t.id === id);
1330
+ if (!match) {
1331
+ throw new Error(
1332
+ `Unknown target "${id}". Choose one of: ${TARGETS.map((t) => t.id).join(", ")}`
1333
+ );
1334
+ }
1335
+ return match;
1336
+ });
1337
+ }
1338
+ function assertSingleFramework(targets) {
1339
+ const grouped = /* @__PURE__ */ new Map();
1340
+ for (const target of targets) {
1341
+ if (!target.group) {
1342
+ continue;
1343
+ }
1344
+ const mates = grouped.get(target.group) ?? [];
1345
+ mates.push(target);
1346
+ grouped.set(target.group, mates);
1347
+ }
1348
+ for (const mates of grouped.values()) {
1349
+ if (mates.length > 1) {
1350
+ throw new Error(
1351
+ `${mates.map((t) => t.label).join(", ")} can't be selected together; pick a single UI framework.`
1352
+ );
1353
+ }
1354
+ }
1355
+ }
1356
+ function mergePackages(targets) {
1357
+ const seen = /* @__PURE__ */ new Set();
1358
+ const merged = [];
1359
+ for (const target of targets) {
1360
+ for (const pkg of target.packages) {
1361
+ if (!seen.has(pkg)) {
1362
+ seen.add(pkg);
1363
+ merged.push(pkg);
1364
+ }
1365
+ }
1366
+ }
1367
+ return merged;
1368
+ }
1369
+
1370
+ // src/run-command.ts
1371
+ import { spawn } from "child_process";
1372
+ function runCommand(command, args, cwd, options) {
1373
+ return new Promise((resolve, reject) => {
1374
+ const isWindows = process.platform === "win32";
1375
+ const stdio = options?.silent ? ["inherit", "ignore", "inherit"] : "inherit";
1376
+ const child = isWindows ? spawn([command, ...args].join(" "), { cwd, stdio, shell: true }) : spawn(command, args, { cwd, stdio });
1377
+ child.on("error", reject);
1378
+ child.on("close", (code) => resolve(code ?? 1));
1379
+ });
1380
+ }
1381
+
1382
+ // src/scaffold.ts
1383
+ import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
1384
+ import { dirname, join as join3 } from "path";
1385
+ function sanitizeProjectName(name) {
1386
+ const cleaned = name.trim().replace(/[^a-zA-Z0-9._-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^-+|-+$/gu, "");
1387
+ return cleaned || "pptx-viewer-app";
1388
+ }
1389
+ function findEntryFile(projectDir, candidates) {
1390
+ for (const candidate of candidates) {
1391
+ if (existsSync3(join3(projectDir, candidate))) {
1392
+ return candidate;
1393
+ }
1394
+ }
1395
+ return null;
1396
+ }
1397
+ async function scaffoldProject(recipe, projectName, pm, cwd) {
1398
+ const scaffoldExit = await runCommand(
1399
+ "npx",
1400
+ ["--yes", recipe.command, ...recipe.args(projectName)],
1401
+ cwd,
1402
+ { silent: true }
1403
+ );
1404
+ if (scaffoldExit !== 0) {
1405
+ throw new Error(`${recipe.command} exited with code ${scaffoldExit}`);
1406
+ }
1407
+ const projectDir = join3(cwd, projectName);
1408
+ const patchedFile = findEntryFile(projectDir, recipe.entryCandidates);
1409
+ if (patchedFile) {
1410
+ writeFileSync(join3(projectDir, patchedFile), recipe.entryContent);
1411
+ }
1412
+ if (recipe.extraFiles) {
1413
+ for (const [relativePath, content] of Object.entries(recipe.extraFiles)) {
1414
+ const fullPath = join3(projectDir, relativePath);
1415
+ mkdirSync(dirname(fullPath), { recursive: true });
1416
+ writeFileSync(fullPath, content);
1417
+ }
1418
+ }
1419
+ if (recipe.extraPackages.length > 0) {
1420
+ const [command, args] = installCommand(pm, recipe.extraPackages);
1421
+ const installExit = await runCommand(command, args, projectDir);
1422
+ if (installExit !== 0) {
1423
+ throw new Error(`${command} exited with code ${installExit}`);
1424
+ }
1425
+ }
1426
+ return { projectDir, patchedFile };
1427
+ }
1428
+
1429
+ // src/orchestrate.ts
1430
+ function printBanner() {
1431
+ console.log(`
1432
+ ${bold(cyan("pptx-viewer"))} ${dim("\xB7 interactive installer")}`);
1433
+ }
1434
+ function printUsage() {
1435
+ printBanner();
1436
+ console.log(`
1437
+ ${bold("Usage:")} npx @christophervr/pptx-viewer [options]
1438
+
1439
+ ${bold("Options:")}
1440
+ ${cyan("--target <ids>")} Skip the picker; comma-separated, any of: ${TARGETS.map((t) => t.id).join(", ")}
1441
+ (the UI bindings - react, vue, angular, svelte, vanilla - are mutually exclusive; pick at most one)
1442
+ ${cyan("--scaffold")} Bootstrap a brand-new starter project instead of installing here
1443
+ ${cyan("--dir <name>")} Project directory name for --scaffold
1444
+ ${cyan("--pm <manager>")} Package manager to use: bun, pnpm, yarn, npm (default: auto-detected)
1445
+ ${cyan("--yes, -y")} Skip confirmation prompts
1446
+ ${cyan("--help, -h")} Show this help
1447
+
1448
+ ${bold("Examples:")}
1449
+ ${gray("npx @christophervr/pptx-viewer")}
1450
+ ${gray("npx @christophervr/pptx-viewer --target react,mcp --yes")}
1451
+ ${gray("npx @christophervr/pptx-viewer --target react --scaffold --dir my-app --yes")}
1452
+ `);
1453
+ }
1454
+ async function resolveTargets(requested) {
1455
+ if (requested) {
1456
+ const targets = findTargetsByIds(parseTargetIds(requested));
1457
+ console.log(`${green("\u2714")} ${targets.map((t) => t.label).join(", ")}`);
1458
+ return targets;
1459
+ }
1460
+ if (!process.stdin.isTTY) {
1461
+ throw new Error("Not running in a terminal: pass --target explicitly (see --help).");
1462
+ }
1463
+ return multiSelect(
1464
+ "What are you building with pptx-viewer? (you can pick more than one)",
1465
+ TARGETS
1466
+ );
1467
+ }
1468
+ async function confirmCompat(cwd, targets) {
1469
+ for (const target of targets) {
1470
+ const result = checkCompat(cwd, target);
1471
+ if (result.compatible) {
1472
+ continue;
1473
+ }
1474
+ console.log(`
1475
+ ${yellow("Warning:")} ${result.message}`);
1476
+ if (process.stdin.isTTY) {
1477
+ const proceed = await confirm("Continue anyway?");
1478
+ if (!proceed) {
1479
+ return false;
1480
+ }
1481
+ }
1482
+ }
1483
+ return true;
1484
+ }
1485
+ async function resolveScaffoldChoice(installTargets, args) {
1486
+ const scaffoldable = installTargets.filter((t) => t.scaffold);
1487
+ if (args.scaffold) {
1488
+ if (scaffoldable.length !== 1) {
1489
+ const scaffoldIds = TARGETS.filter((t) => t.scaffold).map((t) => t.id).join(", ");
1490
+ throw new Error(`--scaffold requires exactly one of: ${scaffoldIds} to be selected.`);
1491
+ }
1492
+ return { useScaffold: true, scaffoldTarget: scaffoldable[0] };
1493
+ }
1494
+ if (scaffoldable.length === 1 && process.stdin.isTTY) {
1495
+ const choice = await selectOption("Install into the current project, or scaffold a new one?", [
1496
+ { label: "Install here", description: "Add the package(s) to the project in this directory" },
1497
+ {
1498
+ label: "Scaffold a new project",
1499
+ description: "Bootstrap a brand-new starter app in its own folder"
1500
+ }
1501
+ ]);
1502
+ return {
1503
+ useScaffold: choice.label === "Scaffold a new project",
1504
+ scaffoldTarget: scaffoldable[0]
1505
+ };
1506
+ }
1507
+ return { useScaffold: false };
1508
+ }
1509
+ async function runScaffoldMode(target, args, configTargets, cwd) {
1510
+ const recipe = target.scaffold;
1511
+ if (!recipe) {
1512
+ throw new Error(`${target.label} has no scaffold recipe.`);
1513
+ }
1514
+ recipe.preflight?.();
1515
+ const optionalPkgs = [];
1516
+ if (recipe.optionalExtras) {
1517
+ for (const extra of recipe.optionalExtras) {
1518
+ const defaultChoice = extra.defaultInclude !== false;
1519
+ const include = args.yes || !process.stdin.isTTY ? defaultChoice : await confirm(extra.prompt);
1520
+ if (include) {
1521
+ optionalPkgs.push(...extra.packages);
1522
+ }
1523
+ }
1524
+ }
1525
+ const effectiveRecipe = optionalPkgs.length > 0 ? { ...recipe, extraPackages: [...recipe.extraPackages, ...optionalPkgs] } : recipe;
1526
+ const defaultName = `pptx-${target.id}-app`;
1527
+ const rawName = args.dir ?? (process.stdin.isTTY ? await input("Project directory name", defaultName) : defaultName);
1528
+ const projectName = sanitizeProjectName(rawName);
1529
+ const pm = args.pm ?? detectPackageManager(cwd);
1530
+ console.log(
1531
+ `
1532
+ ${bold("About to scaffold")} "${cyan(projectName)}" with ${recipe.command} (${target.label}), then install with ${pm}.
1533
+ `
1534
+ );
1535
+ if (!args.yes && process.stdin.isTTY) {
1536
+ const proceed = await confirm("Continue?");
1537
+ if (!proceed) {
1538
+ console.log(`
1539
+ ${dim("Skipped.")}`);
1540
+ return;
1541
+ }
1542
+ }
1543
+ const result = await scaffoldProject(effectiveRecipe, projectName, pm, cwd);
1544
+ if (!result.patchedFile) {
1545
+ console.log(
1546
+ `
1547
+ ${yellow("Scaffolded the project, but could not find an entry file to wire up automatically.")} See the quick-start snippet below and add it yourself.`
1548
+ );
1549
+ }
1550
+ for (const configTarget of configTargets) {
1551
+ console.log(`
1552
+ ${configTarget.nextSteps}
1553
+ `);
1554
+ }
1555
+ console.log(`
1556
+ ${green("\u2714")} ${bold("Done!")} Starting dev server...
1557
+ `);
1558
+ const projectDir = result.projectDir;
1559
+ const devExit = await runCommand(pm, ["run", "dev"], projectDir);
1560
+ if (devExit !== 0) {
1561
+ console.log(`
1562
+ ${cyan(`cd ${projectName}`)}
1563
+ ${cyan(`${pm} run dev`)}
1564
+ `);
1565
+ }
1566
+ }
1567
+ async function runInstallMode(installTargets, configTargets, args, cwd) {
1568
+ if (installTargets.length > 0) {
1569
+ if (!existsSync4(`${cwd}/package.json`)) {
1570
+ throw new Error(
1571
+ `No package.json found in ${cwd}. Run "npm init -y" first, then re-run this command.`
1572
+ );
1573
+ }
1574
+ const proceedPastCompat = await confirmCompat(cwd, installTargets);
1575
+ if (!proceedPastCompat) {
1576
+ console.log(`
1577
+ ${red("Aborted.")}`);
1578
+ return;
1579
+ }
1580
+ const packages = mergePackages(installTargets);
1581
+ const pm = args.pm ?? detectPackageManager(cwd);
1582
+ const [command, cmdArgs] = installCommand(pm, packages);
1583
+ console.log(`
1584
+ ${bold("About to run:")} ${cyan(`${command} ${cmdArgs.join(" ")}`)}
1585
+ `);
1586
+ if (!args.yes && process.stdin.isTTY) {
1587
+ const proceed = await confirm("Install now?");
1588
+ if (!proceed) {
1589
+ console.log(
1590
+ `
1591
+ ${dim("Skipped.")} Run this yourself when ready:
1592
+ ${cyan(`${command} ${cmdArgs.join(" ")}`)}
1593
+ `
1594
+ );
1595
+ return;
1596
+ }
1597
+ }
1598
+ const exitCode = await runCommand(command, cmdArgs, cwd);
1599
+ if (exitCode !== 0) {
1600
+ throw new Error(`${command} exited with code ${exitCode}`);
1601
+ }
1602
+ console.log(`
1603
+ ${green("\u2714")} ${bold("Done.")} Next steps:`);
1604
+ for (const target of installTargets) {
1605
+ console.log(`
1606
+ ${target.nextSteps}
1607
+ `);
1608
+ }
1609
+ }
1610
+ for (const target of configTargets) {
1611
+ console.log(`
1612
+ ${target.nextSteps}
1613
+ `);
1614
+ }
1615
+ }
1616
+ async function runCli() {
1617
+ const args = parseArgs(process.argv.slice(2));
1618
+ if (args.help) {
1619
+ printUsage();
1620
+ return;
1621
+ }
1622
+ printBanner();
1623
+ const targets = await resolveTargets(args.target);
1624
+ assertSingleFramework(targets);
1625
+ const installTargets = targets.filter((t) => t.mode === "install");
1626
+ const configTargets = targets.filter((t) => t.mode === "print-config");
1627
+ const cwd = process.cwd();
1628
+ const { useScaffold, scaffoldTarget } = await resolveScaffoldChoice(installTargets, args);
1629
+ if (useScaffold && scaffoldTarget) {
1630
+ await runScaffoldMode(scaffoldTarget, args, configTargets, cwd);
1631
+ return;
1632
+ }
1633
+ await runInstallMode(installTargets, configTargets, args, cwd);
1634
+ }
1635
+
1636
+ // src/cli.ts
1637
+ runCli().catch((err) => {
1638
+ const message = err instanceof Error ? err.message : String(err);
1639
+ console.error(`${red("\u2718 Error:")} ${message}`);
1640
+ process.exit(1);
1641
+ });