@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/CHANGELOG.md +32 -0
- package/README.md +27 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1642 -0
- package/dist/cli.mjs +1641 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +24 -0
- package/dist/index.mjs +1 -1588
- package/package.json +70 -5
package/dist/index.mjs
CHANGED
|
@@ -1,1589 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
// src/index.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// src/args.ts
|
|
7
|
-
var KNOWN_PMS = ["bun", "pnpm", "yarn", "npm"];
|
|
8
|
-
function readFlagValue(args, index, flag) {
|
|
9
|
-
const value = args[index + 1];
|
|
10
|
-
if (!value) {
|
|
11
|
-
throw new Error(`${flag} needs a value`);
|
|
12
|
-
}
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
function parseArgs(args) {
|
|
16
|
-
const parsed = { help: false, yes: false, scaffold: false };
|
|
17
|
-
for (let i = 0; i < args.length; i++) {
|
|
18
|
-
const arg = args[i];
|
|
19
|
-
switch (arg) {
|
|
20
|
-
case "--help":
|
|
21
|
-
case "-h":
|
|
22
|
-
parsed.help = true;
|
|
23
|
-
break;
|
|
24
|
-
case "--yes":
|
|
25
|
-
case "-y":
|
|
26
|
-
parsed.yes = true;
|
|
27
|
-
break;
|
|
28
|
-
case "--scaffold":
|
|
29
|
-
parsed.scaffold = true;
|
|
30
|
-
break;
|
|
31
|
-
case "--target":
|
|
32
|
-
parsed.target = readFlagValue(args, i, arg);
|
|
33
|
-
i++;
|
|
34
|
-
break;
|
|
35
|
-
case "--dir":
|
|
36
|
-
parsed.dir = readFlagValue(args, i, arg);
|
|
37
|
-
i++;
|
|
38
|
-
break;
|
|
39
|
-
case "--pm": {
|
|
40
|
-
const value = readFlagValue(args, i, arg);
|
|
41
|
-
if (!KNOWN_PMS.includes(value)) {
|
|
42
|
-
throw new Error(`--pm must be one of: ${KNOWN_PMS.join(", ")}`);
|
|
43
|
-
}
|
|
44
|
-
parsed.pm = value;
|
|
45
|
-
i++;
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
default:
|
|
49
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return parsed;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// src/colors.ts
|
|
56
|
-
var isColorEnabled = process.env.NO_COLOR === void 0 && (process.env.FORCE_COLOR !== void 0 || Boolean(process.stdout.isTTY));
|
|
57
|
-
function wrap(open, close) {
|
|
58
|
-
return (text) => isColorEnabled ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
59
|
-
}
|
|
60
|
-
var bold = wrap(1, 22);
|
|
61
|
-
var dim = wrap(2, 22);
|
|
62
|
-
var red = wrap(31, 39);
|
|
63
|
-
var green = wrap(32, 39);
|
|
64
|
-
var yellow = wrap(33, 39);
|
|
65
|
-
var blue = wrap(34, 39);
|
|
66
|
-
var magenta = wrap(35, 39);
|
|
67
|
-
var cyan = wrap(36, 39);
|
|
68
|
-
var gray = wrap(90, 39);
|
|
69
|
-
function isUnicodeSupported() {
|
|
70
|
-
if (process.platform !== "win32") {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
return Boolean(process.env.CI) || Boolean(process.env.WT_SESSION) || Boolean(process.env.ConEmuTask) || process.env.TERM_PROGRAM === "vscode" || process.env.TERM === "xterm-256color";
|
|
74
|
-
}
|
|
75
|
-
var symbols = isUnicodeSupported() ? { pointer: "\u276F", check: "\u2714", cross: "\u2718", radioOn: "\u25C9", radioOff: "\u25EF", bullet: "\xB7" } : { pointer: ">", check: "\u221A", cross: "\xD7", radioOn: "(*)", radioOff: "( )", bullet: "*" };
|
|
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
|
-
@Component({
|
|
418
|
-
selector: 'app-root',
|
|
419
|
-
standalone: true,
|
|
420
|
-
imports: [PowerPointViewerComponent],
|
|
421
|
-
styles: [\`
|
|
422
|
-
:host { display: block; height: 100dvh; }
|
|
423
|
-
.stage { display: flex; align-items: center; justify-content: center; height: 100dvh; padding: 2rem; cursor: default; }
|
|
424
|
-
.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; }
|
|
425
|
-
.dropzone.over, .dropzone:hover { border-color: var(--pptx-primary, #6366f1); background: var(--pptx-muted, rgba(255,255,255,0.04)); }
|
|
426
|
-
h1 { margin: 0; font-size: 1.5rem; font-weight: 500; }
|
|
427
|
-
p { margin: 0; font-size: 0.875rem; color: var(--pptx-muted-foreground, #9ca3af); }
|
|
428
|
-
.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; }
|
|
429
|
-
.pick-label:hover { background: var(--pptx-accent, #374151); }
|
|
430
|
-
.or-sep { font-size: 0.8rem; color: var(--pptx-muted-foreground, #6b7280); }
|
|
431
|
-
.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; }
|
|
432
|
-
.new-btn:hover { opacity: 0.9; }
|
|
433
|
-
\`],
|
|
434
|
-
template: \`
|
|
435
|
-
@if (content(); as c) {
|
|
436
|
-
<div style="height: 100dvh">
|
|
437
|
-
<pptx-power-point-viewer
|
|
438
|
-
[content]="c"
|
|
439
|
-
[canEdit]="true"
|
|
440
|
-
style="height: 100%"
|
|
441
|
-
[collaboration]="collab()"
|
|
442
|
-
(startCollaboration)="collab.set($event)"
|
|
443
|
-
(stopCollaboration)="collab.set(undefined)"
|
|
444
|
-
/>
|
|
445
|
-
</div>
|
|
446
|
-
} @else {
|
|
447
|
-
<div
|
|
448
|
-
class="stage"
|
|
449
|
-
[class.over]="over()"
|
|
450
|
-
(dragover)="$event.preventDefault(); over.set(true)"
|
|
451
|
-
(dragleave)="over.set(false)"
|
|
452
|
-
(drop)="onDrop($event)"
|
|
453
|
-
(click)="fileInput.click()"
|
|
454
|
-
>
|
|
455
|
-
<div class="dropzone">
|
|
456
|
-
<h1>Open a Presentation</h1>
|
|
457
|
-
<p>Drag & drop a .pptx file here, or</p>
|
|
458
|
-
<label class="pick-label" (click)="$event.stopPropagation()">
|
|
459
|
-
Choose .pptx file
|
|
460
|
-
<input #fileInput type="file" accept=".pptx" style="display: none" (change)="onPick($event)" />
|
|
461
|
-
</label>
|
|
462
|
-
<span class="or-sep">or</span>
|
|
463
|
-
<button class="new-btn" (click)="$event.stopPropagation(); newPresentation()">New Presentation</button>
|
|
464
|
-
</div>
|
|
465
|
-
</div>
|
|
466
|
-
}
|
|
467
|
-
\`,
|
|
468
|
-
})
|
|
469
|
-
export class App {
|
|
470
|
-
content = signal<ArrayBuffer | Uint8Array | null>(null);
|
|
471
|
-
collab = signal<CollaborationConfig | undefined>(undefined);
|
|
472
|
-
over = signal(false);
|
|
473
|
-
|
|
474
|
-
async onDrop(e: DragEvent) {
|
|
475
|
-
e.preventDefault();
|
|
476
|
-
this.over.set(false);
|
|
477
|
-
const file = e.dataTransfer?.files?.[0];
|
|
478
|
-
if (file?.name.endsWith('.pptx')) this.content.set(await file.arrayBuffer());
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async onPick(e: Event) {
|
|
482
|
-
const file = (e.target as HTMLInputElement).files?.[0];
|
|
483
|
-
if (file) this.content.set(await file.arrayBuffer());
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async newPresentation() {
|
|
487
|
-
const { handler, data } = await PptxHandler.createBlank({
|
|
488
|
-
title: 'Untitled Presentation',
|
|
489
|
-
initialSlideCount: 1,
|
|
490
|
-
});
|
|
491
|
-
this.content.set(await handler.save(data.slides));
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
`;
|
|
495
|
-
var ANGULAR_MAIN_TS = `import 'zone.js';
|
|
496
|
-
import '@angular/compiler';
|
|
497
|
-
import { bootstrapApplication } from '@angular/platform-browser';
|
|
498
|
-
import { Injectable } from '@angular/core';
|
|
499
|
-
import type { MissingTranslationHandlerParams } from '@ngx-translate/core';
|
|
500
|
-
import { MissingTranslationHandler, provideTranslateService } from '@ngx-translate/core';
|
|
501
|
-
import { keyToLabel } from 'pptx-angular-viewer';
|
|
502
|
-
|
|
503
|
-
import { App } from './app/app.ts';
|
|
504
|
-
|
|
505
|
-
@Injectable()
|
|
506
|
-
class LabelFallbackHandler implements MissingTranslationHandler {
|
|
507
|
-
handle(params: MissingTranslationHandlerParams): string {
|
|
508
|
-
return keyToLabel(params.key);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
bootstrapApplication(App, {
|
|
513
|
-
providers: [
|
|
514
|
-
provideTranslateService({
|
|
515
|
-
lang: 'en',
|
|
516
|
-
fallbackLang: 'en',
|
|
517
|
-
missingTranslationHandler: {
|
|
518
|
-
provide: MissingTranslationHandler,
|
|
519
|
-
useClass: LabelFallbackHandler,
|
|
520
|
-
},
|
|
521
|
-
}),
|
|
522
|
-
],
|
|
523
|
-
}).catch((err) => console.error(err));
|
|
524
|
-
`;
|
|
525
|
-
|
|
526
|
-
// src/templates/react.ts
|
|
527
|
-
var REACT_APP_TSX = `import { useCallback, useState } from 'react';
|
|
528
|
-
import { PptxHandler } from 'pptx-viewer-core';
|
|
529
|
-
import type { CollaborationConfig } from 'pptx-react-viewer';
|
|
530
|
-
import { PowerPointViewer } from 'pptx-react-viewer';
|
|
531
|
-
import 'pptx-react-viewer/styles.css';
|
|
532
|
-
import './i18n';
|
|
533
|
-
|
|
534
|
-
export default function App() {
|
|
535
|
-
const [content, setContent] = useState<Uint8Array | null>(null);
|
|
536
|
-
const [over, setOver] = useState(false);
|
|
537
|
-
const [collab, setCollab] = useState<CollaborationConfig | undefined>();
|
|
538
|
-
|
|
539
|
-
const loadFile = useCallback(async (file: File) => {
|
|
540
|
-
setContent(new Uint8Array(await file.arrayBuffer()));
|
|
541
|
-
}, []);
|
|
542
|
-
|
|
543
|
-
const newPresentation = useCallback(async () => {
|
|
544
|
-
const { handler, data } = await PptxHandler.createBlank({
|
|
545
|
-
title: 'Untitled Presentation',
|
|
546
|
-
initialSlideCount: 1,
|
|
547
|
-
});
|
|
548
|
-
setContent(await handler.save(data.slides));
|
|
549
|
-
}, []);
|
|
550
|
-
|
|
551
|
-
if (content) {
|
|
552
|
-
return (
|
|
553
|
-
<div style={{ height: '100dvh' }}>
|
|
554
|
-
<PowerPointViewer
|
|
555
|
-
content={content}
|
|
556
|
-
canEdit
|
|
557
|
-
collaboration={collab}
|
|
558
|
-
onStartCollaboration={setCollab}
|
|
559
|
-
onStopCollaboration={() => setCollab(undefined)}
|
|
560
|
-
/>
|
|
561
|
-
</div>
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return (
|
|
566
|
-
<div className="stage">
|
|
567
|
-
<div
|
|
568
|
-
className={\`dropzone\${over ? ' over' : ''}\`}
|
|
569
|
-
onDragOver={(e) => { e.preventDefault(); setOver(true); }}
|
|
570
|
-
onDragLeave={() => setOver(false)}
|
|
571
|
-
onDrop={(e) => {
|
|
572
|
-
e.preventDefault();
|
|
573
|
-
setOver(false);
|
|
574
|
-
const file = e.dataTransfer.files[0];
|
|
575
|
-
if (file?.name.endsWith('.pptx')) void loadFile(file);
|
|
576
|
-
}}
|
|
577
|
-
onClick={() => document.getElementById('file-input')?.click()}
|
|
578
|
-
>
|
|
579
|
-
<h1>Open a Presentation</h1>
|
|
580
|
-
<p>Drag & drop a .pptx file here, or</p>
|
|
581
|
-
<label className="pick-label" onClick={(e) => e.stopPropagation()}>
|
|
582
|
-
Choose .pptx file
|
|
583
|
-
<input
|
|
584
|
-
id="file-input"
|
|
585
|
-
type="file"
|
|
586
|
-
accept=".pptx"
|
|
587
|
-
style={{ display: 'none' }}
|
|
588
|
-
onChange={(e) => {
|
|
589
|
-
const file = e.target.files?.[0];
|
|
590
|
-
if (file) void loadFile(file);
|
|
591
|
-
}}
|
|
592
|
-
/>
|
|
593
|
-
</label>
|
|
594
|
-
<span className="or-sep">or</span>
|
|
595
|
-
<button
|
|
596
|
-
className="new-btn"
|
|
597
|
-
onClick={(e) => { e.stopPropagation(); void newPresentation(); }}
|
|
598
|
-
>
|
|
599
|
-
New Presentation
|
|
600
|
-
</button>
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
603
|
-
);
|
|
604
|
-
}
|
|
605
|
-
`;
|
|
606
|
-
var REACT_I18N_TS = `import { createInstance } from 'i18next';
|
|
607
|
-
import { translationsEn, keyToLabel } from 'pptx-react-viewer/i18n';
|
|
608
|
-
import { initReactI18next } from 'react-i18next';
|
|
609
|
-
|
|
610
|
-
const i18n = createInstance();
|
|
611
|
-
|
|
612
|
-
i18n.use(initReactI18next).init({
|
|
613
|
-
resources: {
|
|
614
|
-
en: { translation: translationsEn },
|
|
615
|
-
},
|
|
616
|
-
lng: 'en',
|
|
617
|
-
fallbackLng: 'en',
|
|
618
|
-
interpolation: { escapeValue: false },
|
|
619
|
-
parseMissingKeyHandler: (key: string) => keyToLabel(key),
|
|
620
|
-
missingKeyHandler: false,
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
export default i18n;
|
|
624
|
-
`;
|
|
625
|
-
|
|
626
|
-
// src/templates/shared.ts
|
|
627
|
-
var MINIMAL_APP_CSS = `:root {
|
|
628
|
-
color-scheme: dark;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
*,
|
|
632
|
-
*::before,
|
|
633
|
-
*::after {
|
|
634
|
-
box-sizing: border-box;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
body {
|
|
638
|
-
margin: 0;
|
|
639
|
-
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
640
|
-
overflow-x: hidden;
|
|
641
|
-
background: var(--pptx-background, #030712);
|
|
642
|
-
color: var(--pptx-foreground, #f3f4f6);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
#app,
|
|
646
|
-
#root {
|
|
647
|
-
height: 100dvh;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
.stage {
|
|
651
|
-
display: flex;
|
|
652
|
-
align-items: center;
|
|
653
|
-
justify-content: center;
|
|
654
|
-
height: 100dvh;
|
|
655
|
-
padding: 2rem;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
.dropzone {
|
|
659
|
-
display: flex;
|
|
660
|
-
flex-direction: column;
|
|
661
|
-
align-items: center;
|
|
662
|
-
justify-content: center;
|
|
663
|
-
gap: 0.75rem;
|
|
664
|
-
max-width: 520px;
|
|
665
|
-
width: 100%;
|
|
666
|
-
padding: 3rem;
|
|
667
|
-
text-align: center;
|
|
668
|
-
border: 2px dashed var(--pptx-border, #374151);
|
|
669
|
-
border-radius: 0.75rem;
|
|
670
|
-
cursor: pointer;
|
|
671
|
-
transition:
|
|
672
|
-
border-color 0.15s,
|
|
673
|
-
background 0.15s;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
.dropzone.over,
|
|
677
|
-
.dropzone:hover {
|
|
678
|
-
border-color: var(--pptx-primary, #6366f1);
|
|
679
|
-
background: var(--pptx-muted, rgba(255, 255, 255, 0.04));
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
.dropzone h1 {
|
|
683
|
-
margin: 0;
|
|
684
|
-
font-size: 1.5rem;
|
|
685
|
-
font-weight: 500;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
.dropzone p {
|
|
689
|
-
margin: 0;
|
|
690
|
-
font-size: 0.875rem;
|
|
691
|
-
color: var(--pptx-muted-foreground, #9ca3af);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
.pick-label {
|
|
695
|
-
display: inline-flex;
|
|
696
|
-
align-items: center;
|
|
697
|
-
gap: 0.5rem;
|
|
698
|
-
padding: 0.5rem 1.25rem;
|
|
699
|
-
border-radius: 0.5rem;
|
|
700
|
-
border: 1px solid var(--pptx-border, #374151);
|
|
701
|
-
background: var(--pptx-muted, #1f2937);
|
|
702
|
-
color: var(--pptx-foreground, #f3f4f6);
|
|
703
|
-
cursor: pointer;
|
|
704
|
-
font-size: 0.875rem;
|
|
705
|
-
transition: background 0.15s;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
.pick-label:hover {
|
|
709
|
-
background: var(--pptx-accent, #374151);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
.or-sep {
|
|
713
|
-
font-size: 0.8rem;
|
|
714
|
-
color: var(--pptx-muted-foreground, #6b7280);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.new-btn {
|
|
718
|
-
padding: 0.5rem 1.25rem;
|
|
719
|
-
border-radius: 0.5rem;
|
|
720
|
-
border: none;
|
|
721
|
-
background: var(--pptx-primary, #6366f1);
|
|
722
|
-
color: #fff;
|
|
723
|
-
cursor: pointer;
|
|
724
|
-
font-size: 0.875rem;
|
|
725
|
-
font-weight: 500;
|
|
726
|
-
transition: opacity 0.15s;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
.new-btn:hover {
|
|
730
|
-
opacity: 0.9;
|
|
731
|
-
}
|
|
732
|
-
`;
|
|
733
|
-
var ANGULAR_GLOBAL_CSS = `:root {
|
|
734
|
-
color-scheme: dark;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
*,
|
|
738
|
-
*::before,
|
|
739
|
-
*::after {
|
|
740
|
-
box-sizing: border-box;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
body {
|
|
744
|
-
margin: 0;
|
|
745
|
-
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
746
|
-
background: var(--pptx-background, #030712);
|
|
747
|
-
color: var(--pptx-foreground, #f3f4f6);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
app-root {
|
|
751
|
-
display: block;
|
|
752
|
-
height: 100dvh;
|
|
753
|
-
}
|
|
754
|
-
`;
|
|
755
|
-
|
|
756
|
-
// src/templates/svelte.ts
|
|
757
|
-
var SVELTE_APP_SVELTE = `<script lang="ts">
|
|
758
|
-
import { PptxHandler } from 'pptx-viewer-core';
|
|
759
|
-
import type { CollaborationConfig } from 'pptx-svelte-viewer';
|
|
760
|
-
import { PowerPointViewer } from 'pptx-svelte-viewer';
|
|
761
|
-
|
|
762
|
-
let content = $state<Uint8Array | null>(null);
|
|
763
|
-
let over = $state(false);
|
|
764
|
-
let collab = $state<CollaborationConfig | undefined>();
|
|
765
|
-
|
|
766
|
-
async function loadFile(file: File) {
|
|
767
|
-
content = new Uint8Array(await file.arrayBuffer());
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function onDrop(e: DragEvent) {
|
|
771
|
-
over = false;
|
|
772
|
-
const file = e.dataTransfer?.files?.[0];
|
|
773
|
-
if (file?.name.endsWith('.pptx')) void loadFile(file);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function onPick(e: Event) {
|
|
777
|
-
const file = (e.target as HTMLInputElement).files?.[0];
|
|
778
|
-
if (file) void loadFile(file);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
async function newPresentation() {
|
|
782
|
-
const { handler, data } = await PptxHandler.createBlank({
|
|
783
|
-
title: 'Untitled Presentation',
|
|
784
|
-
initialSlideCount: 1,
|
|
785
|
-
});
|
|
786
|
-
content = await handler.save(data.slides);
|
|
787
|
-
}
|
|
788
|
-
</script>
|
|
789
|
-
|
|
790
|
-
{#if content}
|
|
791
|
-
<div style="height: 100dvh">
|
|
792
|
-
<PowerPointViewer
|
|
793
|
-
source={content}
|
|
794
|
-
editable
|
|
795
|
-
collaboration={collab}
|
|
796
|
-
onstartcollaboration={(cfg) => { collab = cfg; }}
|
|
797
|
-
onstopcollaboration={() => { collab = undefined; }}
|
|
798
|
-
/>
|
|
799
|
-
</div>
|
|
800
|
-
{:else}
|
|
801
|
-
<div
|
|
802
|
-
class="stage"
|
|
803
|
-
ondragover={(e) => { e.preventDefault(); over = true; }}
|
|
804
|
-
ondragleave={() => { over = false; }}
|
|
805
|
-
ondrop={(e) => { e.preventDefault(); onDrop(e); }}
|
|
806
|
-
onclick={() => document.getElementById('file-input')?.click()}
|
|
807
|
-
role="button"
|
|
808
|
-
tabindex="0"
|
|
809
|
-
>
|
|
810
|
-
<div class="dropzone" class:over>
|
|
811
|
-
<h1>Open a Presentation</h1>
|
|
812
|
-
<p>Drag & drop a .pptx file here, or</p>
|
|
813
|
-
<label class="pick-label" onclick={(e) => e.stopPropagation()}>
|
|
814
|
-
Choose .pptx file
|
|
815
|
-
<input id="file-input" type="file" accept=".pptx" style="display: none" onchange={onPick} />
|
|
816
|
-
</label>
|
|
817
|
-
<span class="or-sep">or</span>
|
|
818
|
-
<button class="new-btn" onclick={(e) => { e.stopPropagation(); void newPresentation(); }}>
|
|
819
|
-
New Presentation
|
|
820
|
-
</button>
|
|
821
|
-
</div>
|
|
822
|
-
</div>
|
|
823
|
-
{/if}
|
|
824
|
-
`;
|
|
825
|
-
|
|
826
|
-
// src/templates/vanilla.ts
|
|
827
|
-
var VANILLA_MAIN_TS = `import { createPptxViewer } from 'pptx-vanilla-viewer';
|
|
828
|
-
import { PptxHandler } from 'pptx-viewer-core';
|
|
829
|
-
|
|
830
|
-
import './style.css';
|
|
831
|
-
|
|
832
|
-
const app = document.querySelector<HTMLDivElement>('#app')!;
|
|
833
|
-
|
|
834
|
-
function show(source: ArrayBuffer | Uint8Array): void {
|
|
835
|
-
app.innerHTML = '';
|
|
836
|
-
app.style.height = '100dvh';
|
|
837
|
-
createPptxViewer(app, { source, editable: true });
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function showLanding(): void {
|
|
841
|
-
app.style.height = '';
|
|
842
|
-
app.innerHTML = '';
|
|
843
|
-
|
|
844
|
-
const stage = document.createElement('div');
|
|
845
|
-
stage.className = 'stage';
|
|
846
|
-
|
|
847
|
-
const zone = document.createElement('div');
|
|
848
|
-
zone.className = 'dropzone';
|
|
849
|
-
|
|
850
|
-
const h1 = document.createElement('h1');
|
|
851
|
-
h1.textContent = 'Open a Presentation';
|
|
852
|
-
|
|
853
|
-
const hint = document.createElement('p');
|
|
854
|
-
hint.textContent = 'Drag & drop a .pptx file here, or';
|
|
855
|
-
|
|
856
|
-
const label = document.createElement('label');
|
|
857
|
-
label.className = 'pick-label';
|
|
858
|
-
label.textContent = 'Choose .pptx file';
|
|
859
|
-
|
|
860
|
-
const input = document.createElement('input');
|
|
861
|
-
input.type = 'file';
|
|
862
|
-
input.accept = '.pptx';
|
|
863
|
-
input.style.display = 'none';
|
|
864
|
-
label.append(input);
|
|
865
|
-
|
|
866
|
-
const orSep = document.createElement('span');
|
|
867
|
-
orSep.className = 'or-sep';
|
|
868
|
-
orSep.textContent = 'or';
|
|
869
|
-
|
|
870
|
-
const newBtn = document.createElement('button');
|
|
871
|
-
newBtn.className = 'new-btn';
|
|
872
|
-
newBtn.textContent = 'New Presentation';
|
|
873
|
-
|
|
874
|
-
zone.append(h1, hint, label, orSep, newBtn);
|
|
875
|
-
stage.append(zone);
|
|
876
|
-
app.append(stage);
|
|
877
|
-
|
|
878
|
-
zone.addEventListener('dragover', (e) => {
|
|
879
|
-
e.preventDefault();
|
|
880
|
-
zone.classList.add('over');
|
|
881
|
-
});
|
|
882
|
-
zone.addEventListener('dragleave', () => zone.classList.remove('over'));
|
|
883
|
-
zone.addEventListener('drop', (e) => {
|
|
884
|
-
e.preventDefault();
|
|
885
|
-
zone.classList.remove('over');
|
|
886
|
-
const file = e.dataTransfer?.files?.[0];
|
|
887
|
-
if (file?.name.endsWith('.pptx')) void file.arrayBuffer().then(show);
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
// Click the zone to open the file picker (but not if the button was clicked).
|
|
891
|
-
zone.addEventListener('click', () => input.click());
|
|
892
|
-
label.addEventListener('click', (e) => e.stopPropagation());
|
|
893
|
-
input.addEventListener('click', (e) => e.stopPropagation());
|
|
894
|
-
input.addEventListener('change', () => {
|
|
895
|
-
const file = input.files?.[0];
|
|
896
|
-
if (file) void file.arrayBuffer().then(show);
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
newBtn.addEventListener('click', async (e) => {
|
|
900
|
-
e.stopPropagation();
|
|
901
|
-
newBtn.textContent = 'Creating...';
|
|
902
|
-
newBtn.disabled = true;
|
|
903
|
-
const { handler, data } = await PptxHandler.createBlank({
|
|
904
|
-
title: 'Untitled Presentation',
|
|
905
|
-
initialSlideCount: 1,
|
|
906
|
-
});
|
|
907
|
-
show(await handler.save(data.slides));
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
showLanding();
|
|
912
|
-
`;
|
|
913
|
-
|
|
914
|
-
// src/templates/vue.ts
|
|
915
|
-
var VUE_APP_VUE = `<script setup lang="ts">
|
|
916
|
-
import { ref } from 'vue';
|
|
917
|
-
import { PptxHandler } from 'pptx-viewer-core';
|
|
918
|
-
import type { CollaborationConfig } from 'pptx-vue-viewer';
|
|
919
|
-
import { PowerPointViewer } from 'pptx-vue-viewer';
|
|
920
|
-
import 'pptx-vue-viewer/styles.css';
|
|
921
|
-
|
|
922
|
-
const content = ref<Uint8Array>();
|
|
923
|
-
const over = ref(false);
|
|
924
|
-
const collab = ref<CollaborationConfig | undefined>();
|
|
925
|
-
|
|
926
|
-
async function loadFile(file: File) {
|
|
927
|
-
content.value = new Uint8Array(await file.arrayBuffer());
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
function onDrop(e: DragEvent) {
|
|
931
|
-
over.value = false;
|
|
932
|
-
const file = e.dataTransfer?.files?.[0];
|
|
933
|
-
if (file?.name.endsWith('.pptx')) void loadFile(file);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function onPick(e: Event) {
|
|
937
|
-
const file = (e.target as HTMLInputElement).files?.[0];
|
|
938
|
-
if (file) void loadFile(file);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
async function newPresentation() {
|
|
942
|
-
const { handler, data } = await PptxHandler.createBlank({
|
|
943
|
-
title: 'Untitled Presentation',
|
|
944
|
-
initialSlideCount: 1,
|
|
945
|
-
});
|
|
946
|
-
content.value = await handler.save(data.slides);
|
|
947
|
-
}
|
|
948
|
-
</script>
|
|
949
|
-
|
|
950
|
-
<template>
|
|
951
|
-
<div v-if="content" style="height: 100dvh">
|
|
952
|
-
<PowerPointViewer
|
|
953
|
-
:content="content"
|
|
954
|
-
can-edit
|
|
955
|
-
style="height: 100%"
|
|
956
|
-
:collaboration="collab"
|
|
957
|
-
@start-collaboration="collab = $event"
|
|
958
|
-
@stop-collaboration="collab = undefined"
|
|
959
|
-
/>
|
|
960
|
-
</div>
|
|
961
|
-
<div
|
|
962
|
-
v-else
|
|
963
|
-
class="stage"
|
|
964
|
-
@dragover.prevent="over = true"
|
|
965
|
-
@dragleave="over = false"
|
|
966
|
-
@drop.prevent="onDrop($event as DragEvent)"
|
|
967
|
-
@click="($refs.input as HTMLInputElement).click()"
|
|
968
|
-
>
|
|
969
|
-
<div :class="['dropzone', { over }]">
|
|
970
|
-
<h1>Open a Presentation</h1>
|
|
971
|
-
<p>Drag & drop a .pptx file here, or</p>
|
|
972
|
-
<label class="pick-label" @click.stop>
|
|
973
|
-
Choose .pptx file
|
|
974
|
-
<input ref="input" type="file" accept=".pptx" style="display: none" @change="onPick" />
|
|
975
|
-
</label>
|
|
976
|
-
<span class="or-sep">or</span>
|
|
977
|
-
<button class="new-btn" @click.stop="newPresentation">New Presentation</button>
|
|
978
|
-
</div>
|
|
979
|
-
</div>
|
|
980
|
-
</template>
|
|
981
|
-
|
|
982
|
-
<style>
|
|
983
|
-
:root { color-scheme: dark; }
|
|
984
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
985
|
-
body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--pptx-background, #030712); color: var(--pptx-foreground, #f3f4f6); }
|
|
986
|
-
#app { height: 100dvh; }
|
|
987
|
-
.stage { display: flex; align-items: center; justify-content: center; height: 100dvh; padding: 2rem; }
|
|
988
|
-
.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; }
|
|
989
|
-
.dropzone.over, .dropzone:hover { border-color: var(--pptx-primary, #6366f1); background: var(--pptx-muted, rgba(255, 255, 255, 0.04)); }
|
|
990
|
-
.dropzone h1 { margin: 0; font-size: 1.5rem; font-weight: 500; }
|
|
991
|
-
.dropzone p { margin: 0; font-size: 0.875rem; color: var(--pptx-muted-foreground, #9ca3af); }
|
|
992
|
-
.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; }
|
|
993
|
-
.pick-label:hover { background: var(--pptx-accent, #374151); }
|
|
994
|
-
.or-sep { font-size: 0.8rem; color: var(--pptx-muted-foreground, #6b7280); }
|
|
995
|
-
.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; }
|
|
996
|
-
.new-btn:hover { opacity: 0.9; }
|
|
997
|
-
</style>
|
|
998
|
-
`;
|
|
999
|
-
var VUE_MAIN_TS = `import { createApp } from 'vue';
|
|
1000
|
-
import { createI18n } from 'vue-i18n';
|
|
1001
|
-
import { translationsEn, keyToLabel } from 'pptx-vue-viewer/i18n';
|
|
1002
|
-
import App from './App.vue';
|
|
1003
|
-
|
|
1004
|
-
const i18n = createI18n({
|
|
1005
|
-
legacy: false,
|
|
1006
|
-
locale: 'en',
|
|
1007
|
-
fallbackLocale: 'en',
|
|
1008
|
-
messages: { en: translationsEn },
|
|
1009
|
-
missing: (_locale, key) => keyToLabel(key),
|
|
1010
|
-
missingWarn: false,
|
|
1011
|
-
fallbackWarn: false,
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
createApp(App).use(i18n).mount('#app');
|
|
1015
|
-
`;
|
|
1016
|
-
|
|
1017
|
-
// src/targets.ts
|
|
1018
|
-
var COLLAB_EXTRAS = [
|
|
1019
|
-
{
|
|
1020
|
-
prompt: "Include real-time collaboration? (adds yjs, y-websocket, y-webrtc)",
|
|
1021
|
-
packages: ["yjs", "y-websocket", "y-webrtc"]
|
|
1022
|
-
// defaultInclude is true (the default) - demo apps ship with collab packages.
|
|
1023
|
-
}
|
|
1024
|
-
];
|
|
1025
|
-
var TARGETS = [
|
|
1026
|
-
{
|
|
1027
|
-
id: "react",
|
|
1028
|
-
label: "React",
|
|
1029
|
-
description: "pptx-react-viewer - viewer/editor component for a React 18/19 app",
|
|
1030
|
-
mode: "install",
|
|
1031
|
-
group: "framework",
|
|
1032
|
-
packages: [
|
|
1033
|
-
"pptx-react-viewer",
|
|
1034
|
-
"react",
|
|
1035
|
-
"react-dom",
|
|
1036
|
-
"framer-motion",
|
|
1037
|
-
"lucide-react",
|
|
1038
|
-
"react-icons",
|
|
1039
|
-
"jspdf",
|
|
1040
|
-
"jszip",
|
|
1041
|
-
"fast-xml-parser",
|
|
1042
|
-
"i18next",
|
|
1043
|
-
"react-i18next"
|
|
1044
|
-
],
|
|
1045
|
-
nextSteps: `import { PowerPointViewer } from 'pptx-react-viewer';
|
|
1046
|
-
import 'pptx-react-viewer/styles.css';
|
|
1047
|
-
|
|
1048
|
-
<PowerPointViewer content={arrayBuffer} canEdit />
|
|
1049
|
-
|
|
1050
|
-
Docs: https://www.npmjs.com/package/pptx-react-viewer`,
|
|
1051
|
-
compat: { peerPackage: "react", requiredMajors: [18, 19] },
|
|
1052
|
-
scaffold: {
|
|
1053
|
-
command: "create-vite@latest",
|
|
1054
|
-
// --no-interactive/--no-immediate stop create-vite from prompting for a linter
|
|
1055
|
-
// choice and then auto-installing + auto-starting its own dev server; if it did,
|
|
1056
|
-
// that dev server would block forever and our own entry-file patch + extra
|
|
1057
|
-
// package install below would never run, leaving the default Vite template in place.
|
|
1058
|
-
args: (dir) => [dir, "--template", "react-ts", "--no-interactive", "--no-immediate"],
|
|
1059
|
-
extraPackages: [
|
|
1060
|
-
"pptx-react-viewer",
|
|
1061
|
-
"pptx-viewer-core",
|
|
1062
|
-
"framer-motion",
|
|
1063
|
-
"lucide-react",
|
|
1064
|
-
"react-icons",
|
|
1065
|
-
"jspdf",
|
|
1066
|
-
"jszip",
|
|
1067
|
-
"fast-xml-parser",
|
|
1068
|
-
"i18next",
|
|
1069
|
-
"react-i18next"
|
|
1070
|
-
],
|
|
1071
|
-
entryCandidates: ["src/App.tsx"],
|
|
1072
|
-
entryContent: REACT_APP_TSX,
|
|
1073
|
-
extraFiles: {
|
|
1074
|
-
"src/i18n.ts": REACT_I18N_TS,
|
|
1075
|
-
"src/index.css": MINIMAL_APP_CSS
|
|
1076
|
-
},
|
|
1077
|
-
optionalExtras: COLLAB_EXTRAS
|
|
1078
|
-
}
|
|
1079
|
-
},
|
|
1080
|
-
{
|
|
1081
|
-
id: "vue",
|
|
1082
|
-
label: "Vue",
|
|
1083
|
-
description: "pptx-vue-viewer - viewer/editor component for a Vue 3.5+ app",
|
|
1084
|
-
mode: "install",
|
|
1085
|
-
group: "framework",
|
|
1086
|
-
packages: ["pptx-vue-viewer", "vue", "jszip", "fast-xml-parser"],
|
|
1087
|
-
nextSteps: `<script setup lang="ts">
|
|
1088
|
-
import { PowerPointViewer } from 'pptx-vue-viewer';
|
|
1089
|
-
import 'pptx-vue-viewer/styles.css';
|
|
1090
|
-
</script>
|
|
1091
|
-
|
|
1092
|
-
<template>
|
|
1093
|
-
<PowerPointViewer :content="content" style="height: 100vh" />
|
|
1094
|
-
</template>
|
|
1095
|
-
|
|
1096
|
-
Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
|
|
1097
|
-
compat: { peerPackage: "vue", requiredMajors: [3] },
|
|
1098
|
-
scaffold: {
|
|
1099
|
-
command: "create-vite@latest",
|
|
1100
|
-
args: (dir) => [dir, "--template", "vue-ts", "--no-interactive", "--no-immediate"],
|
|
1101
|
-
extraPackages: [
|
|
1102
|
-
"pptx-vue-viewer",
|
|
1103
|
-
"pptx-viewer-core",
|
|
1104
|
-
"vue-i18n",
|
|
1105
|
-
"jszip",
|
|
1106
|
-
"fast-xml-parser"
|
|
1107
|
-
],
|
|
1108
|
-
entryCandidates: ["src/App.vue"],
|
|
1109
|
-
entryContent: VUE_APP_VUE,
|
|
1110
|
-
extraFiles: { "src/main.ts": VUE_MAIN_TS },
|
|
1111
|
-
optionalExtras: COLLAB_EXTRAS
|
|
1112
|
-
}
|
|
1113
|
-
},
|
|
1114
|
-
{
|
|
1115
|
-
id: "angular",
|
|
1116
|
-
label: "Angular",
|
|
1117
|
-
description: "pptx-angular-viewer - viewer/editor component for an Angular 19-22 app",
|
|
1118
|
-
mode: "install",
|
|
1119
|
-
group: "framework",
|
|
1120
|
-
packages: ["pptx-angular-viewer", "@angular/core", "@angular/common", "rxjs"],
|
|
1121
|
-
nextSteps: `import { PowerPointViewerComponent } from 'pptx-angular-viewer';
|
|
1122
|
-
import 'pptx-angular-viewer/styles.css';
|
|
1123
|
-
|
|
1124
|
-
<pptx-power-point-viewer [content]="content" />
|
|
1125
|
-
|
|
1126
|
-
Docs: https://www.npmjs.com/package/pptx-angular-viewer`,
|
|
1127
|
-
compat: { peerPackage: "@angular/core", requiredMajors: [19, 20, 21, 22] },
|
|
1128
|
-
scaffold: {
|
|
1129
|
-
command: "@angular/cli@latest",
|
|
1130
|
-
// --no-interactive matters even with the flags above supplied: the
|
|
1131
|
-
// `application` schematic's `ssr` option has an `x-prompt`, and `ng new`
|
|
1132
|
-
// prompts for it (plus anything else not already given a value) whenever
|
|
1133
|
-
// stdin is a TTY, which ours is (we inherit the real user's terminal).
|
|
1134
|
-
args: (dir) => [
|
|
1135
|
-
"new",
|
|
1136
|
-
dir,
|
|
1137
|
-
"--standalone",
|
|
1138
|
-
"--skip-git",
|
|
1139
|
-
"--style=css",
|
|
1140
|
-
"--skip-install",
|
|
1141
|
-
"--no-interactive"
|
|
1142
|
-
],
|
|
1143
|
-
extraPackages: ["pptx-angular-viewer", "pptx-viewer-core", "@ngx-translate/core"],
|
|
1144
|
-
// Angular v20+ generates `app.ts`; older schematics generate `app.component.ts`.
|
|
1145
|
-
entryCandidates: ["src/app/app.ts", "src/app/app.component.ts"],
|
|
1146
|
-
entryContent: ANGULAR_APP_TS,
|
|
1147
|
-
extraFiles: { "src/main.ts": ANGULAR_MAIN_TS, "src/styles.css": ANGULAR_GLOBAL_CSS },
|
|
1148
|
-
// @angular/cli@22 requires Node.js >=22.22.0, >=24.13.1, or >=26.0.0. Check
|
|
1149
|
-
// BEFORE the project-name prompt so the user sees the real reason immediately.
|
|
1150
|
-
preflight: () => {
|
|
1151
|
-
const node = process.versions.node;
|
|
1152
|
-
const [maj, min, pat] = node.split(".").map(Number);
|
|
1153
|
-
const ok = maj === 22 && min >= 22 || maj === 24 && (min > 13 || min === 13 && pat >= 1) || maj >= 26;
|
|
1154
|
-
if (!ok) {
|
|
1155
|
-
throw new Error(
|
|
1156
|
-
`@angular/cli@latest requires Node.js v22.22.0+, v24.13.1+, or v26.0.0+.
|
|
1157
|
-
You are running Node.js v${node}.
|
|
1158
|
-
Update Node.js at: https://nodejs.org`
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
},
|
|
1162
|
-
optionalExtras: COLLAB_EXTRAS
|
|
1163
|
-
}
|
|
1164
|
-
},
|
|
1165
|
-
{
|
|
1166
|
-
id: "svelte",
|
|
1167
|
-
label: "Svelte",
|
|
1168
|
-
description: "pptx-svelte-viewer - viewer/editor component for a Svelte 5 app",
|
|
1169
|
-
mode: "install",
|
|
1170
|
-
group: "framework",
|
|
1171
|
-
packages: ["pptx-svelte-viewer", "svelte", "jszip", "fast-xml-parser"],
|
|
1172
|
-
// The Svelte binding compiles its styles into the components, so there
|
|
1173
|
-
// is no `/styles.css` subpath to import.
|
|
1174
|
-
nextSteps: `<script lang="ts">
|
|
1175
|
-
import { PowerPointViewer } from 'pptx-svelte-viewer';
|
|
1176
|
-
</script>
|
|
1177
|
-
|
|
1178
|
-
<PowerPointViewer source={bytes} editable />
|
|
1179
|
-
|
|
1180
|
-
Docs: https://www.npmjs.com/package/pptx-svelte-viewer`,
|
|
1181
|
-
compat: { peerPackage: "svelte", requiredMajors: [5] },
|
|
1182
|
-
scaffold: {
|
|
1183
|
-
command: "create-vite@latest",
|
|
1184
|
-
args: (dir) => [dir, "--template", "svelte-ts", "--no-interactive", "--no-immediate"],
|
|
1185
|
-
extraPackages: ["pptx-svelte-viewer", "pptx-viewer-core", "jszip", "fast-xml-parser"],
|
|
1186
|
-
entryCandidates: ["src/App.svelte"],
|
|
1187
|
-
entryContent: SVELTE_APP_SVELTE,
|
|
1188
|
-
// The starter's main.ts imports ./app.css; replace the Vite demo
|
|
1189
|
-
// styles (centred #app with padding) with a full-viewport reset.
|
|
1190
|
-
extraFiles: { "src/app.css": MINIMAL_APP_CSS },
|
|
1191
|
-
optionalExtras: COLLAB_EXTRAS
|
|
1192
|
-
}
|
|
1193
|
-
},
|
|
1194
|
-
{
|
|
1195
|
-
id: "vanilla",
|
|
1196
|
-
label: "Vanilla JS",
|
|
1197
|
-
description: "pptx-vanilla-viewer - zero-framework viewer/editor, plain DOM, no framework at all",
|
|
1198
|
-
mode: "install",
|
|
1199
|
-
group: "framework",
|
|
1200
|
-
// The vanilla binding injects its own stylesheet at runtime; jszip and
|
|
1201
|
-
// fast-xml-parser are its only peers.
|
|
1202
|
-
packages: ["pptx-vanilla-viewer", "jszip", "fast-xml-parser"],
|
|
1203
|
-
nextSteps: `import { createPptxViewer } from 'pptx-vanilla-viewer';
|
|
1204
|
-
|
|
1205
|
-
const viewer = createPptxViewer(document.getElementById('host')!, {
|
|
1206
|
-
source: '/deck.pptx', // URL, ArrayBuffer, Uint8Array, Blob, or File
|
|
1207
|
-
editable: true,
|
|
1208
|
-
});
|
|
1209
|
-
|
|
1210
|
-
Docs: https://www.npmjs.com/package/pptx-vanilla-viewer`,
|
|
1211
|
-
scaffold: {
|
|
1212
|
-
command: "create-vite@latest",
|
|
1213
|
-
args: (dir) => [dir, "--template", "vanilla-ts", "--no-interactive", "--no-immediate"],
|
|
1214
|
-
extraPackages: [
|
|
1215
|
-
"pptx-vanilla-viewer",
|
|
1216
|
-
"pptx-viewer-core",
|
|
1217
|
-
"three",
|
|
1218
|
-
"jszip",
|
|
1219
|
-
"fast-xml-parser"
|
|
1220
|
-
],
|
|
1221
|
-
entryCandidates: ["src/main.ts"],
|
|
1222
|
-
entryContent: VANILLA_MAIN_TS,
|
|
1223
|
-
// main.ts imports ./style.css; replace the Vite demo styles with a
|
|
1224
|
-
// full-viewport reset.
|
|
1225
|
-
extraFiles: { "src/style.css": MINIMAL_APP_CSS },
|
|
1226
|
-
optionalExtras: COLLAB_EXTRAS
|
|
1227
|
-
}
|
|
1228
|
-
},
|
|
1229
|
-
{
|
|
1230
|
-
id: "core",
|
|
1231
|
-
label: "Core engine only",
|
|
1232
|
-
description: "pptx-viewer-core - framework-agnostic parse/edit/save/convert SDK, no UI",
|
|
1233
|
-
mode: "install",
|
|
1234
|
-
// jszip and fast-xml-parser are regular dependencies of pptx-viewer-core,
|
|
1235
|
-
// so npm/yarn/pnpm/bun pull them in automatically. Nothing else to add.
|
|
1236
|
-
packages: ["pptx-viewer-core"],
|
|
1237
|
-
nextSteps: `import { PptxHandler } from 'pptx-viewer-core';
|
|
1238
|
-
|
|
1239
|
-
const handler = new PptxHandler();
|
|
1240
|
-
const data = await handler.load(arrayBuffer);
|
|
1241
|
-
const bytes = await handler.save(data.slides);
|
|
1242
|
-
|
|
1243
|
-
Docs: https://www.npmjs.com/package/pptx-viewer-core`
|
|
1244
|
-
},
|
|
1245
|
-
{
|
|
1246
|
-
id: "mcp",
|
|
1247
|
-
label: "MCP server",
|
|
1248
|
-
description: "pptx-viewer-mcp - PowerPoint editing tools for AI agents (Claude, Cursor, ...)",
|
|
1249
|
-
mode: "print-config",
|
|
1250
|
-
packages: ["pptx-viewer-mcp"],
|
|
1251
|
-
nextSteps: `Add this to your MCP client config (Claude Desktop, Claude Code, Cursor, ...):
|
|
1252
|
-
|
|
1253
|
-
{
|
|
1254
|
-
"mcpServers": {
|
|
1255
|
-
"pptx": {
|
|
1256
|
-
"command": "npx",
|
|
1257
|
-
"args": ["pptx-viewer-mcp"]
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
npx downloads pptx-viewer-mcp (and its bundled pptx-viewer-core engine) the
|
|
1263
|
-
first time your MCP client starts it, so there is nothing to install by hand.
|
|
1264
|
-
|
|
1265
|
-
Docs: https://www.npmjs.com/package/pptx-viewer-mcp`
|
|
1266
|
-
}
|
|
1267
|
-
];
|
|
1268
|
-
|
|
1269
|
-
// src/resolve.ts
|
|
1270
|
-
function parseTargetIds(csv) {
|
|
1271
|
-
return [
|
|
1272
|
-
...new Set(
|
|
1273
|
-
csv.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
1274
|
-
)
|
|
1275
|
-
];
|
|
1276
|
-
}
|
|
1277
|
-
function findTargetsByIds(ids) {
|
|
1278
|
-
return ids.map((id) => {
|
|
1279
|
-
const match = TARGETS.find((t) => t.id === id);
|
|
1280
|
-
if (!match) {
|
|
1281
|
-
throw new Error(
|
|
1282
|
-
`Unknown target "${id}". Choose one of: ${TARGETS.map((t) => t.id).join(", ")}`
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
return match;
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
function assertSingleFramework(targets) {
|
|
1289
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
1290
|
-
for (const target of targets) {
|
|
1291
|
-
if (!target.group) {
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
const mates = grouped.get(target.group) ?? [];
|
|
1295
|
-
mates.push(target);
|
|
1296
|
-
grouped.set(target.group, mates);
|
|
1297
|
-
}
|
|
1298
|
-
for (const mates of grouped.values()) {
|
|
1299
|
-
if (mates.length > 1) {
|
|
1300
|
-
throw new Error(
|
|
1301
|
-
`${mates.map((t) => t.label).join(", ")} can't be selected together; pick a single UI framework.`
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
function mergePackages(targets) {
|
|
1307
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1308
|
-
const merged = [];
|
|
1309
|
-
for (const target of targets) {
|
|
1310
|
-
for (const pkg of target.packages) {
|
|
1311
|
-
if (!seen.has(pkg)) {
|
|
1312
|
-
seen.add(pkg);
|
|
1313
|
-
merged.push(pkg);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
return merged;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// src/run-command.ts
|
|
1321
|
-
import { spawn } from "child_process";
|
|
1322
|
-
function runCommand(command, args, cwd, options) {
|
|
1323
|
-
return new Promise((resolve, reject) => {
|
|
1324
|
-
const isWindows = process.platform === "win32";
|
|
1325
|
-
const stdio = options?.silent ? ["inherit", "ignore", "inherit"] : "inherit";
|
|
1326
|
-
const child = isWindows ? spawn([command, ...args].join(" "), { cwd, stdio, shell: true }) : spawn(command, args, { cwd, stdio });
|
|
1327
|
-
child.on("error", reject);
|
|
1328
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
1329
|
-
});
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
// src/scaffold.ts
|
|
1333
|
-
import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
1334
|
-
import { dirname, join as join3 } from "path";
|
|
1335
|
-
function sanitizeProjectName(name) {
|
|
1336
|
-
const cleaned = name.trim().replace(/[^a-zA-Z0-9._-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1337
|
-
return cleaned || "pptx-viewer-app";
|
|
1338
|
-
}
|
|
1339
|
-
function findEntryFile(projectDir, candidates) {
|
|
1340
|
-
for (const candidate of candidates) {
|
|
1341
|
-
if (existsSync3(join3(projectDir, candidate))) {
|
|
1342
|
-
return candidate;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
return null;
|
|
1346
|
-
}
|
|
1347
|
-
async function scaffoldProject(recipe, projectName, pm, cwd) {
|
|
1348
|
-
const scaffoldExit = await runCommand(
|
|
1349
|
-
"npx",
|
|
1350
|
-
["--yes", recipe.command, ...recipe.args(projectName)],
|
|
1351
|
-
cwd,
|
|
1352
|
-
{ silent: true }
|
|
1353
|
-
);
|
|
1354
|
-
if (scaffoldExit !== 0) {
|
|
1355
|
-
throw new Error(`${recipe.command} exited with code ${scaffoldExit}`);
|
|
1356
|
-
}
|
|
1357
|
-
const projectDir = join3(cwd, projectName);
|
|
1358
|
-
const patchedFile = findEntryFile(projectDir, recipe.entryCandidates);
|
|
1359
|
-
if (patchedFile) {
|
|
1360
|
-
writeFileSync(join3(projectDir, patchedFile), recipe.entryContent);
|
|
1361
|
-
}
|
|
1362
|
-
if (recipe.extraFiles) {
|
|
1363
|
-
for (const [relativePath, content] of Object.entries(recipe.extraFiles)) {
|
|
1364
|
-
const fullPath = join3(projectDir, relativePath);
|
|
1365
|
-
mkdirSync(dirname(fullPath), { recursive: true });
|
|
1366
|
-
writeFileSync(fullPath, content);
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
if (recipe.extraPackages.length > 0) {
|
|
1370
|
-
const [command, args] = installCommand(pm, recipe.extraPackages);
|
|
1371
|
-
const installExit = await runCommand(command, args, projectDir);
|
|
1372
|
-
if (installExit !== 0) {
|
|
1373
|
-
throw new Error(`${command} exited with code ${installExit}`);
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
return { projectDir, patchedFile };
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// src/index.ts
|
|
1380
|
-
function printBanner() {
|
|
1381
|
-
console.log(`
|
|
1382
|
-
${bold(cyan("pptx-viewer"))} ${dim("\xB7 interactive installer")}`);
|
|
1383
|
-
}
|
|
1384
|
-
function printUsage() {
|
|
1385
|
-
printBanner();
|
|
1386
|
-
console.log(`
|
|
1387
|
-
${bold("Usage:")} npx @christophervr/pptx-viewer [options]
|
|
1388
|
-
|
|
1389
|
-
${bold("Options:")}
|
|
1390
|
-
${cyan("--target <ids>")} Skip the picker; comma-separated, any of: ${TARGETS.map((t) => t.id).join(", ")}
|
|
1391
|
-
(the UI bindings - react, vue, angular, svelte, vanilla - are mutually exclusive; pick at most one)
|
|
1392
|
-
${cyan("--scaffold")} Bootstrap a brand-new starter project instead of installing here
|
|
1393
|
-
${cyan("--dir <name>")} Project directory name for --scaffold
|
|
1394
|
-
${cyan("--pm <manager>")} Package manager to use: bun, pnpm, yarn, npm (default: auto-detected)
|
|
1395
|
-
${cyan("--yes, -y")} Skip confirmation prompts
|
|
1396
|
-
${cyan("--help, -h")} Show this help
|
|
1397
|
-
|
|
1398
|
-
${bold("Examples:")}
|
|
1399
|
-
${gray("npx @christophervr/pptx-viewer")}
|
|
1400
|
-
${gray("npx @christophervr/pptx-viewer --target react,mcp --yes")}
|
|
1401
|
-
${gray("npx @christophervr/pptx-viewer --target react --scaffold --dir my-app --yes")}
|
|
1402
|
-
`);
|
|
1403
|
-
}
|
|
1404
|
-
async function resolveTargets(requested) {
|
|
1405
|
-
if (requested) {
|
|
1406
|
-
const targets = findTargetsByIds(parseTargetIds(requested));
|
|
1407
|
-
console.log(`${green("\u2714")} ${targets.map((t) => t.label).join(", ")}`);
|
|
1408
|
-
return targets;
|
|
1409
|
-
}
|
|
1410
|
-
if (!process.stdin.isTTY) {
|
|
1411
|
-
throw new Error("Not running in a terminal: pass --target explicitly (see --help).");
|
|
1412
|
-
}
|
|
1413
|
-
return multiSelect(
|
|
1414
|
-
"What are you building with pptx-viewer? (you can pick more than one)",
|
|
1415
|
-
TARGETS
|
|
1416
|
-
);
|
|
1417
|
-
}
|
|
1418
|
-
async function confirmCompat(cwd, targets) {
|
|
1419
|
-
for (const target of targets) {
|
|
1420
|
-
const result = checkCompat(cwd, target);
|
|
1421
|
-
if (result.compatible) {
|
|
1422
|
-
continue;
|
|
1423
|
-
}
|
|
1424
|
-
console.log(`
|
|
1425
|
-
${yellow("Warning:")} ${result.message}`);
|
|
1426
|
-
if (process.stdin.isTTY) {
|
|
1427
|
-
const proceed = await confirm("Continue anyway?");
|
|
1428
|
-
if (!proceed) {
|
|
1429
|
-
return false;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
return true;
|
|
1434
|
-
}
|
|
1435
|
-
async function resolveScaffoldChoice(installTargets, args) {
|
|
1436
|
-
const scaffoldable = installTargets.filter((t) => t.scaffold);
|
|
1437
|
-
if (args.scaffold) {
|
|
1438
|
-
if (scaffoldable.length !== 1) {
|
|
1439
|
-
const scaffoldIds = TARGETS.filter((t) => t.scaffold).map((t) => t.id).join(", ");
|
|
1440
|
-
throw new Error(`--scaffold requires exactly one of: ${scaffoldIds} to be selected.`);
|
|
1441
|
-
}
|
|
1442
|
-
return { useScaffold: true, scaffoldTarget: scaffoldable[0] };
|
|
1443
|
-
}
|
|
1444
|
-
if (scaffoldable.length === 1 && process.stdin.isTTY) {
|
|
1445
|
-
const choice = await selectOption("Install into the current project, or scaffold a new one?", [
|
|
1446
|
-
{ label: "Install here", description: "Add the package(s) to the project in this directory" },
|
|
1447
|
-
{
|
|
1448
|
-
label: "Scaffold a new project",
|
|
1449
|
-
description: "Bootstrap a brand-new starter app in its own folder"
|
|
1450
|
-
}
|
|
1451
|
-
]);
|
|
1452
|
-
return {
|
|
1453
|
-
useScaffold: choice.label === "Scaffold a new project",
|
|
1454
|
-
scaffoldTarget: scaffoldable[0]
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
return { useScaffold: false };
|
|
1458
|
-
}
|
|
1459
|
-
async function runScaffoldMode(target, args, configTargets, cwd) {
|
|
1460
|
-
const recipe = target.scaffold;
|
|
1461
|
-
if (!recipe) {
|
|
1462
|
-
throw new Error(`${target.label} has no scaffold recipe.`);
|
|
1463
|
-
}
|
|
1464
|
-
recipe.preflight?.();
|
|
1465
|
-
const optionalPkgs = [];
|
|
1466
|
-
if (recipe.optionalExtras) {
|
|
1467
|
-
for (const extra of recipe.optionalExtras) {
|
|
1468
|
-
const defaultChoice = extra.defaultInclude !== false;
|
|
1469
|
-
const include = args.yes || !process.stdin.isTTY ? defaultChoice : await confirm(extra.prompt);
|
|
1470
|
-
if (include) {
|
|
1471
|
-
optionalPkgs.push(...extra.packages);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
const effectiveRecipe = optionalPkgs.length > 0 ? { ...recipe, extraPackages: [...recipe.extraPackages, ...optionalPkgs] } : recipe;
|
|
1476
|
-
const defaultName = `pptx-${target.id}-app`;
|
|
1477
|
-
const rawName = args.dir ?? (process.stdin.isTTY ? await input("Project directory name", defaultName) : defaultName);
|
|
1478
|
-
const projectName = sanitizeProjectName(rawName);
|
|
1479
|
-
const pm = args.pm ?? detectPackageManager(cwd);
|
|
1480
|
-
console.log(
|
|
1481
|
-
`
|
|
1482
|
-
${bold("About to scaffold")} "${cyan(projectName)}" with ${recipe.command} (${target.label}), then install with ${pm}.
|
|
1483
|
-
`
|
|
1484
|
-
);
|
|
1485
|
-
if (!args.yes && process.stdin.isTTY) {
|
|
1486
|
-
const proceed = await confirm("Continue?");
|
|
1487
|
-
if (!proceed) {
|
|
1488
|
-
console.log(`
|
|
1489
|
-
${dim("Skipped.")}`);
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
const result = await scaffoldProject(effectiveRecipe, projectName, pm, cwd);
|
|
1494
|
-
if (!result.patchedFile) {
|
|
1495
|
-
console.log(
|
|
1496
|
-
`
|
|
1497
|
-
${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.`
|
|
1498
|
-
);
|
|
1499
|
-
}
|
|
1500
|
-
for (const configTarget of configTargets) {
|
|
1501
|
-
console.log(`
|
|
1502
|
-
${configTarget.nextSteps}
|
|
1503
|
-
`);
|
|
1504
|
-
}
|
|
1505
|
-
console.log(`
|
|
1506
|
-
${green("\u2714")} ${bold("Done!")} Starting dev server...
|
|
1507
|
-
`);
|
|
1508
|
-
const projectDir = result.projectDir;
|
|
1509
|
-
const devExit = await runCommand(pm, ["run", "dev"], projectDir);
|
|
1510
|
-
if (devExit !== 0) {
|
|
1511
|
-
console.log(`
|
|
1512
|
-
${cyan(`cd ${projectName}`)}
|
|
1513
|
-
${cyan(`${pm} run dev`)}
|
|
1514
|
-
`);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
async function runInstallMode(installTargets, configTargets, args, cwd) {
|
|
1518
|
-
if (installTargets.length > 0) {
|
|
1519
|
-
if (!existsSync4(`${cwd}/package.json`)) {
|
|
1520
|
-
throw new Error(
|
|
1521
|
-
`No package.json found in ${cwd}. Run "npm init -y" first, then re-run this command.`
|
|
1522
|
-
);
|
|
1523
|
-
}
|
|
1524
|
-
const proceedPastCompat = await confirmCompat(cwd, installTargets);
|
|
1525
|
-
if (!proceedPastCompat) {
|
|
1526
|
-
console.log(`
|
|
1527
|
-
${red("Aborted.")}`);
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
const packages = mergePackages(installTargets);
|
|
1531
|
-
const pm = args.pm ?? detectPackageManager(cwd);
|
|
1532
|
-
const [command, cmdArgs] = installCommand(pm, packages);
|
|
1533
|
-
console.log(`
|
|
1534
|
-
${bold("About to run:")} ${cyan(`${command} ${cmdArgs.join(" ")}`)}
|
|
1535
|
-
`);
|
|
1536
|
-
if (!args.yes && process.stdin.isTTY) {
|
|
1537
|
-
const proceed = await confirm("Install now?");
|
|
1538
|
-
if (!proceed) {
|
|
1539
|
-
console.log(
|
|
1540
|
-
`
|
|
1541
|
-
${dim("Skipped.")} Run this yourself when ready:
|
|
1542
|
-
${cyan(`${command} ${cmdArgs.join(" ")}`)}
|
|
1543
|
-
`
|
|
1544
|
-
);
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
const exitCode = await runCommand(command, cmdArgs, cwd);
|
|
1549
|
-
if (exitCode !== 0) {
|
|
1550
|
-
throw new Error(`${command} exited with code ${exitCode}`);
|
|
1551
|
-
}
|
|
1552
|
-
console.log(`
|
|
1553
|
-
${green("\u2714")} ${bold("Done.")} Next steps:`);
|
|
1554
|
-
for (const target of installTargets) {
|
|
1555
|
-
console.log(`
|
|
1556
|
-
${target.nextSteps}
|
|
1557
|
-
`);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
for (const target of configTargets) {
|
|
1561
|
-
console.log(`
|
|
1562
|
-
${target.nextSteps}
|
|
1563
|
-
`);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
async function main() {
|
|
1567
|
-
const args = parseArgs(process.argv.slice(2));
|
|
1568
|
-
if (args.help) {
|
|
1569
|
-
printUsage();
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
printBanner();
|
|
1573
|
-
const targets = await resolveTargets(args.target);
|
|
1574
|
-
assertSingleFramework(targets);
|
|
1575
|
-
const installTargets = targets.filter((t) => t.mode === "install");
|
|
1576
|
-
const configTargets = targets.filter((t) => t.mode === "print-config");
|
|
1577
|
-
const cwd = process.cwd();
|
|
1578
|
-
const { useScaffold, scaffoldTarget } = await resolveScaffoldChoice(installTargets, args);
|
|
1579
|
-
if (useScaffold && scaffoldTarget) {
|
|
1580
|
-
await runScaffoldMode(scaffoldTarget, args, configTargets, cwd);
|
|
1581
|
-
return;
|
|
1582
|
-
}
|
|
1583
|
-
await runInstallMode(installTargets, configTargets, args, cwd);
|
|
1584
|
-
}
|
|
1585
|
-
main().catch((err) => {
|
|
1586
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1587
|
-
console.error(`${red("\u2718 Error:")} ${message}`);
|
|
1588
|
-
process.exit(1);
|
|
1589
|
-
});
|
|
2
|
+
export * from "pptx-react-viewer";
|