@office-kit/pptx-dev 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuichiro Yamashita
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # @office-kit/pptx-dev
2
+
3
+ Local TSX authoring tools for `@office-kit/pptx-dsl`. Build and preview an editable
4
+ PowerPoint presentation while changing its source in VSCode or Claude Code.
5
+ Requires Node.js 22.18 or later.
6
+
7
+ The CLI and DSL packages are **not yet published to npm**. Start with the
8
+ [step-by-step authoring guide](https://office-kit.github.io/pptx/docs/authoring)
9
+ for a source-checkout installation. In the repository root (after
10
+ `pnpm install --frozen-lockfile`), run:
11
+
12
+ ```sh
13
+ pnpm --filter @office-kit/pptx-dev... build
14
+ PPTX_REPO="$PWD"
15
+ PPTX_PACKS="$PPTX_REPO/.probe/packages"
16
+ mkdir -p "$PPTX_PACKS"
17
+ pnpm --filter @office-kit/pptx-dev... exec pnpm pack --pack-destination "$PPTX_PACKS"
18
+ node "$PPTX_REPO/packages/dev/dist/cli.mjs" init ../my-slides
19
+ cd ../my-slides
20
+ npm install --legacy-peer-deps "$PPTX_PACKS"/*.tgz
21
+ npm run check
22
+ npm run dev
23
+ ```
24
+
25
+ Install all four tarballs from the same checkout. The temporary peer override is
26
+ needed because the DSL targets the next core release, whose version number has
27
+ not been bumped yet; the source checkout contains the required core changes.
28
+ Keep the tarballs for reinstalls, and use an empty pack directory after upgrading
29
+ so old versions are not included. The guide also covers Claude Code, VSCode,
30
+ templates, viewing controls, and troubleshooting.
31
+
32
+ Open the local URL printed by the server. Save `deck.tsx` to rebuild. The viewer
33
+ has a vertical thumbnail strip and a large slide canvas. Click a thumbnail or use
34
+ arrow keys, Page Up/Down, Home/End to navigate. Fit/zoom and Present (Escape to
35
+ exit) are viewing controls; the canvas has no editing, dragging or resize handles.
36
+ Changes are made only in TSX, including when an AI agent edits the presentation.
37
+ Download PPTX exports the last successful build.
38
+ A syntax or runtime error is shown without discarding the last successful preview.
39
+ DSL evaluation errors include the TSX element's source file and line number.
40
+
41
+ ```sh
42
+ npm run check
43
+ npm run build
44
+ ```
45
+
46
+ `check` runs TypeScript; `build` writes `deck.pptx`. Compilation alone does not
47
+ perform TypeScript checking, so run both before delivery. Exported charts, text,
48
+ images and tables remain editable. The renderer previews the serialized PPTX,
49
+ but is not a guarantee of identical rendering in PowerPoint.
50
+
51
+ The generated `CLAUDE.md` explains authoring conventions. VSCode picks up the
52
+ included `tsconfig.json` for completion and diagnostics. Start the **Preview
53
+ presentation** task, then use **Simple Browser: Show** with the printed URL and
54
+ move it to a side editor group. No custom editor extension is needed for this
55
+ workflow. Preview-to-source selection is not implemented.
56
+
57
+ ## Commands
58
+
59
+ - `office-pptx init <new-directory>` creates a TSX project without overwriting an existing directory.
60
+ - `office-pptx dev <deck.tsx> [--port 4173]` watches the entry file's directory recursively.
61
+ - `office-pptx build <deck.tsx> [--out deck.pptx]` writes a PPTX.
62
+ - `office-pptx inspect <template.pptx>` prints slides, shape targets and layout references as JSON.
63
+
64
+ Keep imported components, source PPTX files and images inside the watched
65
+ directory. `node_modules`, `.git`, `dist` and `.office-kit` are excluded. Load
66
+ assets with `readFile(new URL('./asset.png', import.meta.url))`; the compiler
67
+ preserves the original URL of each bundled source module.
68
+
69
+ TSX runs as trusted local Node code, including imports and Raw callbacks.
70
+ A fresh worker evaluates each build, with a 60-second limit, so module state
71
+ cannot accumulate across updates. Workers are not a security sandbox. The
72
+ preview server binds to `127.0.0.1` and rejects unexpected Host headers.
73
+
74
+ Programmatic exports: `buildDeck`, `exportDeck`, `initProject`, `inspectTemplate`.
75
+ `buildDeck` returns PPTX bytes, SVG slides, aspect ratio, module dependencies and
76
+ core validation diagnostics. Browser APIs and DOM globals are not available in
77
+ TSX evaluation.
78
+
79
+ ## Developing in this monorepo
80
+
81
+ The new packages must be built locally before running the CLI; they need not be
82
+ published to use this checkout.
83
+
84
+ ```sh
85
+ pnpm --filter @office-kit/pptx-dev... build
86
+ node packages/dev/dist/cli.mjs dev packages/dsl/examples/review.tsx
87
+ node packages/dev/dist/cli.mjs build packages/dsl/examples/review.tsx --out review.pptx
88
+ pnpm --filter @office-kit/pptx-dev test
89
+ ```
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ import { i as initProject, n as exportDeck, r as inspectTemplate, t as buildDeck } from "./src-BA02cMY9.mjs";
3
+ import { dirname, resolve, sep } from "node:path";
4
+ import { createServer } from "node:http";
5
+ import { watch } from "node:fs";
6
+ //#region src/page.ts
7
+ const page = `<!doctype html>
8
+ <html lang="en">
9
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width">
10
+ <title>Office Kit — PowerPoint preview</title>
11
+ <style>
12
+ *{box-sizing:border-box}
13
+ body{margin:0;height:100dvh;overflow:hidden;background:#e9ecf1;color:#202735;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}
14
+ button,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #d4d9e2;border-radius:6px;background:#fff;padding:7px 12px;text-decoration:none;cursor:pointer}
15
+ button:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}
16
+ :focus-visible{outline:2px solid #4967dd;outline-offset:3px}
17
+ header{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}
18
+ .brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}
19
+ #status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}
20
+ .workspace{display:grid;grid-template-columns:224px minmax(0,1fr);min-height:0}
21
+ .filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}
22
+ .filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}
23
+ #thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}
24
+ .thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}
25
+ .slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#737d8e;padding-top:5px}
26
+ .thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #19212d18;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}
27
+ .thumbnail[aria-current="true"]{background:#e7edff}.thumbnail[aria-current="true"] img{border-color:#4967dd}.thumbnail[aria-current="true"] .slide-number{color:#3654c1;font-weight:700}
28
+ main{min-width:0;min-height:0;display:flex;flex-direction:column}
29
+ #error{flex:none;max-height:30%;overflow:auto;background:#fff0ef;color:#922e25;margin:0;padding:16px 20px;white-space:pre-wrap;border-bottom:1px solid #e8bbb7}
30
+ #error[hidden]{display:none}
31
+ #stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}
32
+ #slide{flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}
33
+ #slide iframe{display:block;width:100%;height:100%;border:0;pointer-events:none}
34
+ #empty{margin:auto;color:#737d8e}
35
+ footer{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}
36
+ #count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}
37
+ #presentation-controls{display:none}
38
+ body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
39
+ .presenting header,.presenting footer,.presenting .filmstrip{display:none}
40
+ .presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}
41
+ .presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}
42
+ .presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}
43
+ #presentation-controls button{background:transparent;color:white;border-color:#5d6575}
44
+ @media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}
45
+ @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
46
+ </style>
47
+ <header><span class="brand">Office Kit</span><span class="badge">Preview</span><span id="status" role="status">Building…</span><button id="present" disabled>Present</button><a class="download" href="/deck.pptx">Download PPTX</a></header>
48
+ <div class="workspace">
49
+ <nav class="filmstrip" aria-label="Slides"><h2>Slides</h2><ol id="thumbnails"></ol></nav>
50
+ <main aria-label="Slide viewer"><pre id="error" role="alert" hidden></pre><div id="stage" tabindex="-1"><div id="empty">Waiting for slides…</div><div id="slide" hidden></div></div></main>
51
+ </div>
52
+ <footer><span id="count" aria-live="polite">No slides</span><span class="hint">Changes appear automatically · View only</span><button id="prev" aria-label="Previous slide" disabled>‹</button><button id="next" aria-label="Next slide" disabled>›</button><label for="zoom">Zoom</label><select id="zoom"><option value="fit">Fit</option><option value="0.5">50%</option><option value="0.75">75%</option><option value="1">100%</option><option value="1.25">125%</option><option value="1.5">150%</option><option value="2">200%</option></select></footer>
53
+ <div id="presentation-controls"><button id="present-prev" aria-label="Previous slide">‹</button><span id="present-count"></span><button id="present-next" aria-label="Next slide">›</button><button id="exit-present">Exit · Esc</button></div>
54
+ <script>
55
+ let state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;
56
+ const byId=id=>document.getElementById(id);
57
+ const stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');
58
+ function resize(){
59
+ if(!state.slides.length)return;
60
+ const style=getComputedStyle(stage);
61
+ const width=Math.max(1,stage.clientWidth-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight));
62
+ const height=Math.max(1,stage.clientHeight-parseFloat(style.paddingTop)-parseFloat(style.paddingBottom));
63
+ const ratio=state.aspectRatio;
64
+ const zoom=byId('zoom').value;
65
+ const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);
66
+ slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';
67
+ }
68
+ function selectSlide(next,focusThumbnail=false){
69
+ index=Math.max(0,Math.min(next,state.slides.length-1));
70
+ const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';
71
+ byId('count').textContent=count;byId('present-count').textContent=count;
72
+ for(const id of ['prev','present-prev'])byId(id).disabled=index===0;
73
+ for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;
74
+ byId('present').disabled=!state.slides.length;
75
+ byId('zoom').disabled=!state.slides.length;
76
+ slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;
77
+ slide.replaceChildren();
78
+ if(state.slides[index]){
79
+ const frame=document.createElement('iframe');frame.sandbox='';frame.tabIndex=-1;frame.title='Slide '+(index+1);
80
+ frame.srcdoc='<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}svg{display:block;width:100%;height:100%}</style>'+state.slides[index];
81
+ slide.append(frame);
82
+ }
83
+ for(const [position,item] of Array.from(thumbnails.children).entries()){
84
+ const button=item.firstElementChild,selected=position===index;
85
+ button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;
86
+ if(selected){button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}
87
+ }
88
+ resize();
89
+ }
90
+ function update(updated){
91
+ const focusedThumbnail=thumbnails.contains(document.activeElement);
92
+ const changed=updated.slides.length!==state.slides.length||updated.slides.some((svg,i)=>svg!==state.slides[i]);
93
+ state=updated;
94
+ byId('status').textContent=state.error?'Build failed · showing last successful output':state.slides.length+' slides · Live';
95
+ byId('error').textContent=state.error||'';byId('error').hidden=!state.error;
96
+ document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));
97
+ if(changed){
98
+ for(const url of urls)URL.revokeObjectURL(url);
99
+ urls=state.slides.map(svg=>URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'})));
100
+ thumbnails.replaceChildren(...urls.map((url,i)=>{
101
+ const item=document.createElement('li'),button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');
102
+ button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);
103
+ number.className='slide-number';number.textContent=String(i+1);
104
+ image.src=url;image.alt='';image.draggable=false;image.loading='lazy';
105
+ button.append(number,image);item.append(button);return item;
106
+ }));
107
+ selectSlide(index,focusedThumbnail);
108
+ }else resize();
109
+ if(!state.slides.length&&presenting)void exitPresentation();
110
+ }
111
+ function setPresenting(value){
112
+ presenting=value;document.body.classList.toggle('presenting',value);resize();
113
+ if(value)stage.focus();else{
114
+ byId('present').focus();
115
+ thumbnails.children[index]?.firstElementChild.scrollIntoView({block:'nearest'});
116
+ }
117
+ }
118
+ async function exitPresentation(){
119
+ setPresenting(false);
120
+ if(document.fullscreenElement)await document.exitFullscreen();
121
+ }
122
+ byId('present').onclick=async()=>{
123
+ setPresenting(true);
124
+ try{await document.documentElement.requestFullscreen();}
125
+ catch{byId('exit-present').textContent='Exit view · Esc';}
126
+ };
127
+ byId('exit-present').onclick=exitPresentation;
128
+ document.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement&&presenting)setPresenting(false);});
129
+ for(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);
130
+ for(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);
131
+ byId('zoom').onchange=resize;
132
+ stage.onclick=()=>{if(presenting)selectSlide(index+1);};
133
+ document.addEventListener('keydown',event=>{
134
+ if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}
135
+ if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;
136
+ let next=index;
137
+ if(['ArrowLeft','ArrowUp','PageUp'].includes(event.key))next--;
138
+ else if(['ArrowRight','ArrowDown','PageDown'].includes(event.key))next++;
139
+ else if(event.key==='Home')next=0;
140
+ else if(event.key==='End')next=state.slides.length-1;
141
+ else if(event.key===' '&&presenting&&!event.target.closest('button,a'))next+=event.shiftKey?-1:1;
142
+ else return;
143
+ event.preventDefault();selectSlide(next,thumbnails.contains(document.activeElement));
144
+ });
145
+ new ResizeObserver(resize).observe(stage);
146
+ let refreshId=0;
147
+ async function refresh(){
148
+ const id=++refreshId;
149
+ try{const response=await fetch('/state');if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId)update(updated);}
150
+ catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
151
+ }
152
+ const events=new EventSource('/events');events.onmessage=refresh;events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
153
+ <\/script></html>`;
154
+ //#endregion
155
+ //#region src/server.ts
156
+ async function serveDeck(entry, port = 4173) {
157
+ let latest;
158
+ let error = null;
159
+ let building = false;
160
+ let pending = false;
161
+ let closed = false;
162
+ const clients = /* @__PURE__ */ new Set();
163
+ const server = createServer((request, response) => {
164
+ const host = request.headers.host;
165
+ if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {
166
+ response.writeHead(403).end();
167
+ return;
168
+ }
169
+ response.setHeader("Cache-Control", "no-store");
170
+ if (request.url === "/events") {
171
+ response.writeHead(200, { "Content-Type": "text/event-stream" });
172
+ response.write("data: ready\n\n");
173
+ clients.add(response);
174
+ request.on("close", () => clients.delete(response));
175
+ } else if (request.url === "/state") {
176
+ response.writeHead(200, { "Content-Type": "application/json" });
177
+ response.end(JSON.stringify({
178
+ slides: latest?.slides ?? [],
179
+ aspectRatio: latest?.aspectRatio ?? 16 / 9,
180
+ error,
181
+ diagnostics: latest?.diagnostics ?? []
182
+ }));
183
+ } else if (request.url === "/deck.pptx" && latest) {
184
+ response.writeHead(200, {
185
+ "Content-Type": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
186
+ "Content-Disposition": "attachment; filename=\"deck.pptx\""
187
+ });
188
+ response.end(latest.bytes);
189
+ } else if (request.url === "/") {
190
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
191
+ response.end(page);
192
+ } else response.writeHead(404).end();
193
+ });
194
+ async function rebuild() {
195
+ if (closed) return;
196
+ if (building) {
197
+ pending = true;
198
+ return;
199
+ }
200
+ building = true;
201
+ try {
202
+ latest = await buildDeck(entry);
203
+ error = null;
204
+ } catch (cause) {
205
+ error = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
206
+ } finally {
207
+ building = false;
208
+ for (const client of clients) client.write("data: updated\n\n");
209
+ if (pending) {
210
+ pending = false;
211
+ rebuild();
212
+ }
213
+ }
214
+ }
215
+ let timer;
216
+ const watcher = watch(dirname(resolve(entry)), { recursive: true }, (_, filename) => {
217
+ if (!filename || filename.split(sep).some((part) => [
218
+ "node_modules",
219
+ ".git",
220
+ "dist",
221
+ ".office-kit"
222
+ ].includes(part))) return;
223
+ if (!/\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;
224
+ clearTimeout(timer);
225
+ timer = setTimeout(() => {
226
+ rebuild();
227
+ }, 100);
228
+ });
229
+ let actualPort = port;
230
+ try {
231
+ await new Promise((resolveListen, reject) => {
232
+ server.once("error", reject);
233
+ server.listen(port, "127.0.0.1", () => {
234
+ server.off("error", reject);
235
+ resolveListen();
236
+ });
237
+ });
238
+ } catch (cause) {
239
+ watcher.close();
240
+ throw cause;
241
+ }
242
+ const address = server.address();
243
+ if (address && typeof address !== "string") actualPort = address.port;
244
+ await rebuild();
245
+ return {
246
+ url: `http://127.0.0.1:${actualPort}`,
247
+ async close() {
248
+ closed = true;
249
+ clearTimeout(timer);
250
+ watcher.close();
251
+ for (const client of clients) client.end();
252
+ await new Promise((done, reject) => server.close((cause) => cause ? reject(cause) : done()));
253
+ }
254
+ };
255
+ }
256
+ //#endregion
257
+ //#region src/cli.ts
258
+ const usage = `Usage: office-pptx init <new-directory>
259
+ office-pptx dev <deck.tsx> [--port 4173]
260
+ office-pptx build <deck.tsx> [--out deck.pptx]
261
+ office-pptx inspect <template.pptx>`;
262
+ const [command, entry, ...args] = process.argv.slice(2);
263
+ try {
264
+ if (command === "--help" || command === "-h") console.log(usage);
265
+ else {
266
+ if (!entry) throw new Error(usage);
267
+ const option = command === "build" ? "--out" : command === "dev" ? "--port" : void 0;
268
+ if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) throw new Error(`Unexpected arguments: ${args.join(" ")}\n${usage}`);
269
+ if (command === "init") {
270
+ const directory = await initProject(entry);
271
+ console.log(`Created ${directory}\nRun npm install, then npm run dev inside that directory.`);
272
+ } else if (command === "inspect") console.log(JSON.stringify(await inspectTemplate(entry), null, 2));
273
+ else if (command === "build") {
274
+ const output = resolve(args[1] ?? "deck.pptx");
275
+ const result = await exportDeck(entry, output);
276
+ console.log(`Wrote ${output} (${result.slides.length} slides)`);
277
+ } else if (command === "dev") {
278
+ const port = args[1] === void 0 ? 4173 : Number(args[1]);
279
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error("Port must be an integer between 0 and 65535.");
280
+ const server = await serveDeck(entry, port);
281
+ console.log(`Preview: ${server.url}`);
282
+ const stop = () => {
283
+ server.close().then(() => process.exit(0));
284
+ };
285
+ process.once("SIGINT", stop);
286
+ process.once("SIGTERM", stop);
287
+ } else throw new Error(usage);
288
+ }
289
+ } catch (cause) {
290
+ console.error(cause instanceof Error ? cause.message : cause);
291
+ process.exitCode = 1;
292
+ }
293
+ //#endregion
294
+ export {};
295
+
296
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/page.ts","../src/server.ts","../src/cli.ts"],"sourcesContent":["export const page = `<!doctype html>\n<html lang=\"en\">\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\">\n<title>Office Kit — PowerPoint preview</title>\n<style>\n*{box-sizing:border-box}\nbody{margin:0;height:100dvh;overflow:hidden;background:#e9ecf1;color:#202735;font:13px system-ui;display:grid;grid-template-rows:60px minmax(0,1fr) 42px}\nbutton,a,select{font:inherit;color:inherit}button,select,.download{border:1px solid #d4d9e2;border-radius:6px;background:#fff;padding:7px 12px;text-decoration:none;cursor:pointer}\nbutton:hover:not(:disabled),.download:hover{background:#f0f3f9}button:disabled{opacity:.4;cursor:default}\n:focus-visible{outline:2px solid #4967dd;outline-offset:3px}\nheader{display:flex;align-items:center;gap:16px;padding:0 20px;background:#fff;border-bottom:1px solid #d4d9e2;min-width:0}\n.brand{font-weight:700;font-size:17px;white-space:nowrap}.badge{font-size:11px;color:#616b7c;background:#f1f3f7;border-radius:4px;padding:3px 6px}\n#status{flex:1;color:#667085;min-width:0}#present{background:#293c73;color:#fff;border-color:#293c73}\n.workspace{display:grid;grid-template-columns:224px minmax(0,1fr);min-height:0}\n.filmstrip{background:#f7f8fa;border-right:1px solid #d4d9e2;overflow:auto;overscroll-behavior:contain;padding:16px 12px}\n.filmstrip h2{margin:0 0 12px 26px;text-transform:uppercase;letter-spacing:.1em;font-size:10px;color:#7c8596;font-weight:600}\n#thumbnails{display:flex;flex-direction:column;gap:12px;margin:0;padding:0;list-style:none}\n.thumbnail{display:flex;align-items:flex-start;gap:8px;width:100%;padding:4px 3px;border:0;background:transparent;border-radius:5px;text-align:left}\n.slide-number{width:18px;flex:none;text-align:right;font-size:11px;color:#737d8e;padding-top:5px}\n.thumbnail img{display:block;min-width:0;width:calc(100% - 26px);background:white;box-shadow:0 1px 4px #19212d18;border:2px solid transparent;border-radius:3px;aspect-ratio:var(--slide-ratio,16/9);object-fit:contain}\n.thumbnail[aria-current=\"true\"]{background:#e7edff}.thumbnail[aria-current=\"true\"] img{border-color:#4967dd}.thumbnail[aria-current=\"true\"] .slide-number{color:#3654c1;font-weight:700}\nmain{min-width:0;min-height:0;display:flex;flex-direction:column}\n#error{flex:none;max-height:30%;overflow:auto;background:#fff0ef;color:#922e25;margin:0;padding:16px 20px;white-space:pre-wrap;border-bottom:1px solid #e8bbb7}\n#error[hidden]{display:none}\n#stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}\n#slide{flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}\n#slide iframe{display:block;width:100%;height:100%;border:0;pointer-events:none}\n#empty{margin:auto;color:#737d8e}\nfooter{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}\n#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}\n#presentation-controls{display:none}\nbody.presenting{grid-template-rows:minmax(0,1fr);background:#111}\n.presenting header,.presenting footer,.presenting .filmstrip{display:none}\n.presenting .workspace{grid-template-columns:minmax(0,1fr)}.presenting #stage{padding:0;background:#111}.presenting #slide{box-shadow:none}\n.presenting #presentation-controls{display:flex;position:fixed;bottom:16px;left:50%;transform:translateX(-50%);align-items:center;gap:12px;background:#202735e8;color:white;padding:6px;border-radius:8px;opacity:0;transition:opacity .15s}\n.presenting #presentation-controls:hover,.presenting #presentation-controls:focus-within{opacity:1}\n#presentation-controls button{background:transparent;color:white;border-color:#5d6575}\n@media(max-width:700px){.workspace{grid-template-columns:140px minmax(0,1fr)}.filmstrip{padding:12px 5px}header{padding:0 12px;gap:10px}.badge,footer .hint{display:none}#stage{padding:16px}footer{gap:8px}#status{font-size:11px}.download{padding:7px 8px}}\n@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}\n</style>\n<header><span class=\"brand\">Office Kit</span><span class=\"badge\">Preview</span><span id=\"status\" role=\"status\">Building…</span><button id=\"present\" disabled>Present</button><a class=\"download\" href=\"/deck.pptx\">Download PPTX</a></header>\n<div class=\"workspace\">\n<nav class=\"filmstrip\" aria-label=\"Slides\"><h2>Slides</h2><ol id=\"thumbnails\"></ol></nav>\n<main aria-label=\"Slide viewer\"><pre id=\"error\" role=\"alert\" hidden></pre><div id=\"stage\" tabindex=\"-1\"><div id=\"empty\">Waiting for slides…</div><div id=\"slide\" hidden></div></div></main>\n</div>\n<footer><span id=\"count\" aria-live=\"polite\">No slides</span><span class=\"hint\">Changes appear automatically · View only</span><button id=\"prev\" aria-label=\"Previous slide\" disabled>‹</button><button id=\"next\" aria-label=\"Next slide\" disabled>›</button><label for=\"zoom\">Zoom</label><select id=\"zoom\"><option value=\"fit\">Fit</option><option value=\"0.5\">50%</option><option value=\"0.75\">75%</option><option value=\"1\">100%</option><option value=\"1.25\">125%</option><option value=\"1.5\">150%</option><option value=\"2\">200%</option></select></footer>\n<div id=\"presentation-controls\"><button id=\"present-prev\" aria-label=\"Previous slide\">‹</button><span id=\"present-count\"></span><button id=\"present-next\" aria-label=\"Next slide\">›</button><button id=\"exit-present\">Exit · Esc</button></div>\n<script>\nlet state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;\nconst byId=id=>document.getElementById(id);\nconst stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');\nfunction resize(){\n if(!state.slides.length)return;\n const style=getComputedStyle(stage);\n const width=Math.max(1,stage.clientWidth-parseFloat(style.paddingLeft)-parseFloat(style.paddingRight));\n const height=Math.max(1,stage.clientHeight-parseFloat(style.paddingTop)-parseFloat(style.paddingBottom));\n const ratio=state.aspectRatio;\n const zoom=byId('zoom').value;\n const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);\n slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';\n}\nfunction selectSlide(next,focusThumbnail=false){\n index=Math.max(0,Math.min(next,state.slides.length-1));\n const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';\n byId('count').textContent=count;byId('present-count').textContent=count;\n for(const id of ['prev','present-prev'])byId(id).disabled=index===0;\n for(const id of ['next','present-next'])byId(id).disabled=index>=state.slides.length-1;\n byId('present').disabled=!state.slides.length;\n byId('zoom').disabled=!state.slides.length;\n slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;\n slide.replaceChildren();\n if(state.slides[index]){\n const frame=document.createElement('iframe');frame.sandbox='';frame.tabIndex=-1;frame.title='Slide '+(index+1);\n frame.srcdoc='<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}svg{display:block;width:100%;height:100%}</style>'+state.slides[index];\n slide.append(frame);\n }\n for(const [position,item] of Array.from(thumbnails.children).entries()){\n const button=item.firstElementChild,selected=position===index;\n button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;\n if(selected){button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}\n }\n resize();\n}\nfunction update(updated){\n const focusedThumbnail=thumbnails.contains(document.activeElement);\n const changed=updated.slides.length!==state.slides.length||updated.slides.some((svg,i)=>svg!==state.slides[i]);\n state=updated;\n byId('status').textContent=state.error?'Build failed · showing last successful output':state.slides.length+' slides · Live';\n byId('error').textContent=state.error||'';byId('error').hidden=!state.error;\n document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));\n if(changed){\n for(const url of urls)URL.revokeObjectURL(url);\n urls=state.slides.map(svg=>URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'})));\n thumbnails.replaceChildren(...urls.map((url,i)=>{\n const item=document.createElement('li'),button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');\n button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);\n number.className='slide-number';number.textContent=String(i+1);\n image.src=url;image.alt='';image.draggable=false;image.loading='lazy';\n button.append(number,image);item.append(button);return item;\n }));\n selectSlide(index,focusedThumbnail);\n }else resize();\n if(!state.slides.length&&presenting)void exitPresentation();\n}\nfunction setPresenting(value){\n presenting=value;document.body.classList.toggle('presenting',value);resize();\n if(value)stage.focus();else{\n byId('present').focus();\n thumbnails.children[index]?.firstElementChild.scrollIntoView({block:'nearest'});\n }\n}\nasync function exitPresentation(){\n setPresenting(false);\n if(document.fullscreenElement)await document.exitFullscreen();\n}\nbyId('present').onclick=async()=>{\n setPresenting(true);\n try{await document.documentElement.requestFullscreen();}\n catch{byId('exit-present').textContent='Exit view · Esc';}\n};\nbyId('exit-present').onclick=exitPresentation;\ndocument.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement&&presenting)setPresenting(false);});\nfor(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);\nfor(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);\nbyId('zoom').onchange=resize;\nstage.onclick=()=>{if(presenting)selectSlide(index+1);};\ndocument.addEventListener('keydown',event=>{\n if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}\n if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;\n let next=index;\n if(['ArrowLeft','ArrowUp','PageUp'].includes(event.key))next--;\n else if(['ArrowRight','ArrowDown','PageDown'].includes(event.key))next++;\n else if(event.key==='Home')next=0;\n else if(event.key==='End')next=state.slides.length-1;\n else if(event.key===' '&&presenting&&!event.target.closest('button,a'))next+=event.shiftKey?-1:1;\n else return;\n event.preventDefault();selectSlide(next,thumbnails.contains(document.activeElement));\n});\nnew ResizeObserver(resize).observe(stage);\nlet refreshId=0;\nasync function refresh(){\n const id=++refreshId;\n try{const response=await fetch('/state');if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId)update(updated);}\n catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}\n}\nconst events=new EventSource('/events');events.onmessage=refresh;events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();\n</script></html>`;\n","import { createServer, type ServerResponse } from 'node:http';\nimport { watch } from 'node:fs';\nimport { dirname, resolve, sep } from 'node:path';\nimport { buildDeck, type BuildResult } from './index.ts';\nimport { page } from './page.ts';\n\nexport async function serveDeck(entry: string, port = 4173) {\n let latest: BuildResult | undefined;\n let error: string | null = null;\n let building = false;\n let pending = false;\n let closed = false;\n const clients = new Set<ServerResponse>();\n const server = createServer((request, response) => {\n const host = request.headers.host;\n if (host !== `127.0.0.1:${actualPort}` && host !== `localhost:${actualPort}`) {\n response.writeHead(403).end();\n return;\n }\n response.setHeader('Cache-Control', 'no-store');\n if (request.url === '/events') {\n response.writeHead(200, { 'Content-Type': 'text/event-stream' });\n response.write('data: ready\\n\\n');\n clients.add(response);\n request.on('close', () => clients.delete(response));\n } else if (request.url === '/state') {\n response.writeHead(200, { 'Content-Type': 'application/json' });\n response.end(\n JSON.stringify({\n slides: latest?.slides ?? [],\n aspectRatio: latest?.aspectRatio ?? 16 / 9,\n error,\n diagnostics: latest?.diagnostics ?? [],\n }),\n );\n } else if (request.url === '/deck.pptx' && latest) {\n response.writeHead(200, {\n 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'Content-Disposition': 'attachment; filename=\"deck.pptx\"',\n });\n response.end(latest.bytes);\n } else if (request.url === '/') {\n response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n response.end(page);\n } else {\n response.writeHead(404).end();\n }\n });\n async function rebuild() {\n if (closed) return;\n if (building) {\n pending = true;\n return;\n }\n building = true;\n try {\n latest = await buildDeck(entry);\n error = null;\n } catch (cause) {\n error = cause instanceof Error ? (cause.stack ?? cause.message) : String(cause);\n } finally {\n building = false;\n for (const client of clients) client.write('data: updated\\n\\n');\n if (pending) {\n pending = false;\n void rebuild();\n }\n }\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const root = dirname(resolve(entry));\n const watcher = watch(root, { recursive: true }, (_, filename) => {\n if (\n !filename ||\n filename\n .split(sep)\n .some((part) => ['node_modules', '.git', 'dist', '.office-kit'].includes(part))\n )\n return;\n if (!/\\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;\n clearTimeout(timer);\n timer = setTimeout(() => {\n void rebuild();\n }, 100);\n });\n let actualPort = port;\n try {\n await new Promise<void>((resolveListen, reject) => {\n server.once('error', reject);\n server.listen(port, '127.0.0.1', () => {\n server.off('error', reject);\n resolveListen();\n });\n });\n } catch (cause) {\n watcher.close();\n throw cause;\n }\n const address = server.address();\n if (address && typeof address !== 'string') actualPort = address.port;\n await rebuild();\n return {\n url: `http://127.0.0.1:${actualPort}`,\n async close() {\n closed = true;\n clearTimeout(timer);\n watcher.close();\n for (const client of clients) client.end();\n await new Promise<void>((done, reject) =>\n server.close((cause) => (cause ? reject(cause) : done())),\n );\n },\n };\n}\n","#!/usr/bin/env node\nimport { resolve } from 'node:path';\nimport { exportDeck } from './index.ts';\nimport { serveDeck } from './server.ts';\nimport { initProject } from './init.ts';\nimport { inspectTemplate } from './inspect.ts';\n\nconst usage = `Usage: office-pptx init <new-directory>\n office-pptx dev <deck.tsx> [--port 4173]\n office-pptx build <deck.tsx> [--out deck.pptx]\n office-pptx inspect <template.pptx>`;\nconst [command, entry, ...args] = process.argv.slice(2);\ntry {\n if (command === '--help' || command === '-h') {\n console.log(usage);\n } else {\n if (!entry) throw new Error(usage);\n const option = command === 'build' ? '--out' : command === 'dev' ? '--port' : undefined;\n if (args.length && (args.length !== 2 || !option || args[0] !== option || !args[1])) {\n throw new Error(`Unexpected arguments: ${args.join(' ')}\\n${usage}`);\n }\n if (command === 'init') {\n const directory = await initProject(entry);\n console.log(`Created ${directory}\\nRun npm install, then npm run dev inside that directory.`);\n } else if (command === 'inspect') {\n console.log(JSON.stringify(await inspectTemplate(entry), null, 2));\n } else if (command === 'build') {\n const output = resolve(args[1] ?? 'deck.pptx');\n const result = await exportDeck(entry, output);\n console.log(`Wrote ${output} (${result.slides.length} slides)`);\n } else if (command === 'dev') {\n const port = args[1] === undefined ? 4173 : Number(args[1]);\n if (!Number.isInteger(port) || port < 0 || port > 65535)\n throw new Error('Port must be an integer between 0 and 65535.');\n const server = await serveDeck(entry, port);\n console.log(`Preview: ${server.url}`);\n const stop = () => {\n void server.close().then(() => process.exit(0));\n };\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n } else {\n throw new Error(usage);\n }\n }\n} catch (cause) {\n console.error(cause instanceof Error ? cause.message : cause);\n process.exitCode = 1;\n}\n"],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { validatePresentation } from "@office-kit/pptx";
2
+
3
+ //#region src/build.d.ts
4
+ interface BuildResult {
5
+ bytes: Uint8Array;
6
+ slides: string[];
7
+ aspectRatio: number;
8
+ dependencies: string[];
9
+ diagnostics: ReturnType<typeof validatePresentation>;
10
+ }
11
+ //#endregion
12
+ //#region src/init.d.ts
13
+ /** Creates a new directory; existing projects are never overwritten. */
14
+ declare function initProject(directory: string): Promise<string>;
15
+ //#endregion
16
+ //#region src/inspect.d.ts
17
+ /** Source references usable directly in Slide, Fill and Remove props. */
18
+ declare function inspectTemplate(file: string): Promise<{
19
+ slides: {
20
+ index: number;
21
+ part: string;
22
+ title: string | null;
23
+ shapes: {
24
+ id: number;
25
+ name: string;
26
+ kind: "shape" | "picture" | "group" | "graphicFrame" | "connector";
27
+ placeholder: {
28
+ type: string | null;
29
+ idx: number | null;
30
+ };
31
+ }[];
32
+ }[];
33
+ layouts: {
34
+ part: string;
35
+ name: string;
36
+ type: string | null;
37
+ }[];
38
+ }>;
39
+ //#endregion
40
+ //#region src/index.d.ts
41
+ /** Each evaluation gets a fresh module cache and releases it when finished. */
42
+ declare function buildDeck(entry: string): Promise<BuildResult>;
43
+ declare function exportDeck(entry: string, output: string): Promise<BuildResult>;
44
+ //#endregion
45
+ export { type BuildResult, buildDeck, exportDeck, initProject, inspectTemplate };
46
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { i as initProject, n as exportDeck, r as inspectTemplate, t as buildDeck } from "./src-BA02cMY9.mjs";
2
+ export { buildDeck, exportDeck, initProject, inspectTemplate };
@@ -0,0 +1,204 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { getShapeId, getShapeKind, getShapeName, getShapePlaceholderIdx, getShapePlaceholderType, getSlideLayoutName, getSlideLayoutPartName, getSlideLayoutType, getSlideLayouts, getSlidePartName, getSlideShapes, getSlideTitle, getSlides, loadPresentation } from "@office-kit/pptx";
5
+ //#region src/init.ts
6
+ const starter = `import { Presentation, Slide, Text, Shape } from '@office-kit/pptx-dsl';
7
+
8
+ export default (
9
+ <Presentation>
10
+ <Slide background="#15171C">
11
+ <Shape preset="rect" x={0.9} y={2.55} width={0.14} height={1.75} fill="#E5481F" stroke={false} />
12
+ <Text x={1.25} y={2.4} width={10} height={1.1} size={48} bold color="#FFFFFF">
13
+ Your next presentation
14
+ </Text>
15
+ <Text x={1.25} y={3.55} width={10} height={0.6} size={22} color="#B4B9C4">
16
+ Edit this TSX and save to update the preview.
17
+ </Text>
18
+ </Slide>
19
+ </Presentation>
20
+ );
21
+ `;
22
+ const guide = `# Writing this presentation
23
+
24
+ Edit deck.tsx and local TypeScript components. The JSX runtime is
25
+ @office-kit/pptx-dsl, with no React or Vue dependency.
26
+
27
+ - Run npm run dev, then open its local URL to preview. Saving source files
28
+ updates the preview. A failed build retains the last successful output.
29
+ - The preview is view-only: use the vertical thumbnails to select slides, zoom
30
+ to inspect details, and Present to view full-screen (Escape exits). Make all
31
+ content and layout changes in TSX; there are no canvas editing controls.
32
+ - Run npm run check for TypeScript diagnostics; run npm run build to export
33
+ deck.pptx. Inspect every slide in the preview after changes.
34
+ - Coordinates and dimensions are inches; font sizes and stroke widths are points.
35
+ The default 16:9 canvas is 13.333 × 7.5 inches.
36
+ - Use native Text, Shape, Image, Table and Chart elements. Use JS functions,
37
+ arrays and map to compose elements; do not import React.
38
+ - Chart takes the core ChartSpec in its spec prop. Text accepts rich ParagraphSpec
39
+ arrays through paragraphs. Raw is an escape hatch for public core APIs.
40
+ - For templates: readFile(new URL('./template.pptx', import.meta.url)), then use
41
+ Presentation source={bytes} mode="edit" and Slide target={{index: 0}}.
42
+ Fill target={{name: 'Title 1'}} changes an existing shape. Indices are zero-based.
43
+ Use office-pptx inspect template.pptx to discover names, layouts and placeholders.
44
+ - Use mode="compose" and Slide from={{index: 0}} to build a new sequence from a
45
+ source deck. This intentionally removes the original slide sequence.
46
+ - Keep assets inside this project so the dev server sees changes. Use source-relative
47
+ new URL('./image.png', import.meta.url) when loading local assets.
48
+ - Preserve existing slides and formatting unless the request calls for changes.
49
+ Never rasterize a chart/table as a shortcut. Raw-only features are not DSL coverage.
50
+ - Preview is a rendering aid, not a guarantee of identical PowerPoint rendering.
51
+ Check the exported file in the target application for final delivery.
52
+
53
+ VSCode uses tsconfig.json for completion and diagnostics. Start the
54
+ "Preview presentation" task, then run "Simple Browser: Show" with the printed URL
55
+ and move that editor to a side group. Type errors appear in the Problems panel.
56
+ `;
57
+ /** Creates a new directory; existing projects are never overwritten. */
58
+ async function initProject(directory) {
59
+ const root = resolve(directory);
60
+ const packageLocations = [
61
+ import.meta.resolve("@office-kit/pptx/package.json"),
62
+ import.meta.resolve("@office-kit/pptx-dsl/package.json"),
63
+ new URL("../package.json", import.meta.url)
64
+ ];
65
+ const versions = await Promise.all(packageLocations.map(async (location) => {
66
+ return `^${JSON.parse(await readFile(new URL(location), "utf8")).version}`;
67
+ }));
68
+ await mkdir(root);
69
+ const files = {
70
+ "package.json": JSON.stringify({
71
+ name: "my-presentation",
72
+ private: true,
73
+ type: "module",
74
+ scripts: {
75
+ dev: "office-pptx dev deck.tsx",
76
+ build: "office-pptx build deck.tsx --out deck.pptx",
77
+ check: "tsc --noEmit"
78
+ },
79
+ dependencies: {
80
+ "@office-kit/pptx": versions[0],
81
+ "@office-kit/pptx-dsl": versions[1]
82
+ },
83
+ devDependencies: {
84
+ "@office-kit/pptx-dev": versions[2],
85
+ "@types/node": "^24.13.2",
86
+ typescript: "^6.0.3"
87
+ }
88
+ }, null, 2) + "\n",
89
+ "tsconfig.json": JSON.stringify({
90
+ compilerOptions: {
91
+ target: "ES2022",
92
+ module: "ESNext",
93
+ moduleResolution: "Bundler",
94
+ jsx: "react-jsx",
95
+ jsxImportSource: "@office-kit/pptx-dsl",
96
+ strict: true,
97
+ exactOptionalPropertyTypes: true,
98
+ noEmit: true,
99
+ allowImportingTsExtensions: true,
100
+ types: ["node"]
101
+ },
102
+ include: ["**/*.ts", "**/*.tsx"],
103
+ exclude: ["node_modules"]
104
+ }, null, 2) + "\n",
105
+ "deck.tsx": starter,
106
+ "CLAUDE.md": guide,
107
+ ".gitignore": "node_modules/\ndeck.pptx\n",
108
+ ".vscode/tasks.json": JSON.stringify({
109
+ version: "2.0.0",
110
+ tasks: [
111
+ {
112
+ label: "Preview presentation",
113
+ type: "shell",
114
+ command: "npm run dev",
115
+ isBackground: true,
116
+ problemMatcher: {
117
+ pattern: {
118
+ regexp: "^NEVER_MATCH$",
119
+ file: 1,
120
+ message: 2
121
+ },
122
+ background: {
123
+ activeOnStart: true,
124
+ beginsPattern: "^.*office-pptx dev",
125
+ endsPattern: "^Preview: "
126
+ }
127
+ }
128
+ },
129
+ {
130
+ label: "Check presentation",
131
+ type: "shell",
132
+ command: "npm run check",
133
+ problemMatcher: "$tsc"
134
+ },
135
+ {
136
+ label: "Export presentation",
137
+ type: "shell",
138
+ command: "npm run build",
139
+ problemMatcher: []
140
+ }
141
+ ]
142
+ }, null, 2) + "\n"
143
+ };
144
+ await mkdir(join(root, ".vscode"));
145
+ await Promise.all(Object.entries(files).map(([name, content]) => writeFile(join(root, name), content, { flag: "wx" })));
146
+ return root;
147
+ }
148
+ //#endregion
149
+ //#region src/inspect.ts
150
+ /** Source references usable directly in Slide, Fill and Remove props. */
151
+ async function inspectTemplate(file) {
152
+ const presentation = await loadPresentation(await readFile(file));
153
+ return {
154
+ slides: getSlides(presentation).map((slide, index) => ({
155
+ index,
156
+ part: getSlidePartName(slide),
157
+ title: getSlideTitle(slide),
158
+ shapes: getSlideShapes(slide).map((shape) => ({
159
+ id: getShapeId(shape),
160
+ name: getShapeName(shape),
161
+ kind: getShapeKind(shape),
162
+ placeholder: {
163
+ type: getShapePlaceholderType(shape),
164
+ idx: getShapePlaceholderIdx(shape)
165
+ }
166
+ }))
167
+ })),
168
+ layouts: getSlideLayouts(presentation).map((layout) => ({
169
+ part: getSlideLayoutPartName(layout),
170
+ name: getSlideLayoutName(layout),
171
+ type: getSlideLayoutType(layout)
172
+ }))
173
+ };
174
+ }
175
+ //#endregion
176
+ //#region src/index.ts
177
+ /** Each evaluation gets a fresh module cache and releases it when finished. */
178
+ async function buildDeck(entry) {
179
+ const worker = new Worker(new URL("./worker.mjs", import.meta.url), {
180
+ workerData: { entry: resolve(entry) },
181
+ execArgv: ["--enable-source-maps"]
182
+ });
183
+ let timer;
184
+ try {
185
+ return await new Promise((resolveBuild, reject) => {
186
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Build exceeded 60 seconds. Check the deck for loops or unresolved promises.")), 6e4);
187
+ worker.once("message", (result) => resolveBuild(result));
188
+ worker.once("error", reject);
189
+ worker.once("exit", (code) => reject(/* @__PURE__ */ new Error(`Build worker exited without a result (code ${code}).`)));
190
+ });
191
+ } finally {
192
+ clearTimeout(timer);
193
+ await worker.terminate();
194
+ }
195
+ }
196
+ async function exportDeck(entry, output) {
197
+ const result = await buildDeck(entry);
198
+ await writeFile(output, result.bytes);
199
+ return result;
200
+ }
201
+ //#endregion
202
+ export { initProject as i, exportDeck as n, inspectTemplate as r, buildDeck as t };
203
+
204
+ //# sourceMappingURL=src-BA02cMY9.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src-BA02cMY9.mjs","names":[],"sources":["../src/init.ts","../src/inspect.ts","../src/index.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nconst starter = `import { Presentation, Slide, Text, Shape } from '@office-kit/pptx-dsl';\n\nexport default (\n <Presentation>\n <Slide background=\"#15171C\">\n <Shape preset=\"rect\" x={0.9} y={2.55} width={0.14} height={1.75} fill=\"#E5481F\" stroke={false} />\n <Text x={1.25} y={2.4} width={10} height={1.1} size={48} bold color=\"#FFFFFF\">\n Your next presentation\n </Text>\n <Text x={1.25} y={3.55} width={10} height={0.6} size={22} color=\"#B4B9C4\">\n Edit this TSX and save to update the preview.\n </Text>\n </Slide>\n </Presentation>\n);\n`;\n\nconst guide = `# Writing this presentation\n\nEdit deck.tsx and local TypeScript components. The JSX runtime is\n@office-kit/pptx-dsl, with no React or Vue dependency.\n\n- Run npm run dev, then open its local URL to preview. Saving source files\n updates the preview. A failed build retains the last successful output.\n- The preview is view-only: use the vertical thumbnails to select slides, zoom\n to inspect details, and Present to view full-screen (Escape exits). Make all\n content and layout changes in TSX; there are no canvas editing controls.\n- Run npm run check for TypeScript diagnostics; run npm run build to export\n deck.pptx. Inspect every slide in the preview after changes.\n- Coordinates and dimensions are inches; font sizes and stroke widths are points.\n The default 16:9 canvas is 13.333 × 7.5 inches.\n- Use native Text, Shape, Image, Table and Chart elements. Use JS functions,\n arrays and map to compose elements; do not import React.\n- Chart takes the core ChartSpec in its spec prop. Text accepts rich ParagraphSpec\n arrays through paragraphs. Raw is an escape hatch for public core APIs.\n- For templates: readFile(new URL('./template.pptx', import.meta.url)), then use\n Presentation source={bytes} mode=\"edit\" and Slide target={{index: 0}}.\n Fill target={{name: 'Title 1'}} changes an existing shape. Indices are zero-based.\n Use office-pptx inspect template.pptx to discover names, layouts and placeholders.\n- Use mode=\"compose\" and Slide from={{index: 0}} to build a new sequence from a\n source deck. This intentionally removes the original slide sequence.\n- Keep assets inside this project so the dev server sees changes. Use source-relative\n new URL('./image.png', import.meta.url) when loading local assets.\n- Preserve existing slides and formatting unless the request calls for changes.\n Never rasterize a chart/table as a shortcut. Raw-only features are not DSL coverage.\n- Preview is a rendering aid, not a guarantee of identical PowerPoint rendering.\n Check the exported file in the target application for final delivery.\n\nVSCode uses tsconfig.json for completion and diagnostics. Start the\n\"Preview presentation\" task, then run \"Simple Browser: Show\" with the printed URL\nand move that editor to a side group. Type errors appear in the Problems panel.\n`;\n\n/** Creates a new directory; existing projects are never overwritten. */\nexport async function initProject(directory: string): Promise<string> {\n const root = resolve(directory);\n const packageLocations = [\n import.meta.resolve('@office-kit/pptx/package.json'),\n import.meta.resolve('@office-kit/pptx-dsl/package.json'),\n new URL('../package.json', import.meta.url),\n ];\n const versions = await Promise.all(\n packageLocations.map(async (location) => {\n const manifest: { version: string } = JSON.parse(await readFile(new URL(location), 'utf8'));\n return `^${manifest.version}`;\n }),\n );\n await mkdir(root);\n const files: Record<string, string> = {\n 'package.json':\n JSON.stringify(\n {\n name: 'my-presentation',\n private: true,\n type: 'module',\n scripts: {\n dev: 'office-pptx dev deck.tsx',\n build: 'office-pptx build deck.tsx --out deck.pptx',\n check: 'tsc --noEmit',\n },\n dependencies: { '@office-kit/pptx': versions[0]!, '@office-kit/pptx-dsl': versions[1]! },\n devDependencies: {\n '@office-kit/pptx-dev': versions[2]!,\n '@types/node': '^24.13.2',\n typescript: '^6.0.3',\n },\n },\n null,\n 2,\n ) + '\\n',\n 'tsconfig.json':\n JSON.stringify(\n {\n compilerOptions: {\n target: 'ES2022',\n module: 'ESNext',\n moduleResolution: 'Bundler',\n jsx: 'react-jsx',\n jsxImportSource: '@office-kit/pptx-dsl',\n strict: true,\n exactOptionalPropertyTypes: true,\n noEmit: true,\n allowImportingTsExtensions: true,\n types: ['node'],\n },\n include: ['**/*.ts', '**/*.tsx'],\n exclude: ['node_modules'],\n },\n null,\n 2,\n ) + '\\n',\n 'deck.tsx': starter,\n 'CLAUDE.md': guide,\n '.gitignore': 'node_modules/\\ndeck.pptx\\n',\n '.vscode/tasks.json':\n JSON.stringify(\n {\n version: '2.0.0',\n tasks: [\n {\n label: 'Preview presentation',\n type: 'shell',\n command: 'npm run dev',\n isBackground: true,\n problemMatcher: {\n pattern: { regexp: '^NEVER_MATCH$', file: 1, message: 2 },\n background: {\n activeOnStart: true,\n beginsPattern: '^.*office-pptx dev',\n endsPattern: '^Preview: ',\n },\n },\n },\n {\n label: 'Check presentation',\n type: 'shell',\n command: 'npm run check',\n problemMatcher: '$tsc',\n },\n {\n label: 'Export presentation',\n type: 'shell',\n command: 'npm run build',\n problemMatcher: [],\n },\n ],\n },\n null,\n 2,\n ) + '\\n',\n };\n await mkdir(join(root, '.vscode'));\n await Promise.all(\n Object.entries(files).map(([name, content]) =>\n writeFile(join(root, name), content, { flag: 'wx' }),\n ),\n );\n return root;\n}\n","import { readFile } from 'node:fs/promises';\nimport {\n loadPresentation,\n getSlides,\n getSlidePartName,\n getSlideTitle,\n getSlideShapes,\n getShapeName,\n getShapeId,\n getShapeKind,\n getShapePlaceholderType,\n getShapePlaceholderIdx,\n getSlideLayouts,\n getSlideLayoutName,\n getSlideLayoutPartName,\n getSlideLayoutType,\n} from '@office-kit/pptx';\n\n/** Source references usable directly in Slide, Fill and Remove props. */\nexport async function inspectTemplate(file: string) {\n const presentation = await loadPresentation(await readFile(file));\n return {\n slides: getSlides(presentation).map((slide, index) => ({\n index,\n part: getSlidePartName(slide),\n title: getSlideTitle(slide),\n shapes: getSlideShapes(slide).map((shape) => ({\n id: getShapeId(shape),\n name: getShapeName(shape),\n kind: getShapeKind(shape),\n placeholder: { type: getShapePlaceholderType(shape), idx: getShapePlaceholderIdx(shape) },\n })),\n })),\n layouts: getSlideLayouts(presentation).map((layout) => ({\n part: getSlideLayoutPartName(layout),\n name: getSlideLayoutName(layout),\n type: getSlideLayoutType(layout),\n })),\n };\n}\n","import { Worker } from 'node:worker_threads';\nimport { writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport type { BuildResult } from './build.ts';\nexport type { BuildResult } from './build.ts';\nexport { initProject } from './init.ts';\nexport { inspectTemplate } from './inspect.ts';\n\n/** Each evaluation gets a fresh module cache and releases it when finished. */\nexport async function buildDeck(entry: string): Promise<BuildResult> {\n const worker = new Worker(new URL('./worker.mjs', import.meta.url), {\n workerData: { entry: resolve(entry) },\n execArgv: ['--enable-source-maps'],\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await new Promise<BuildResult>((resolveBuild, reject) => {\n timer = setTimeout(\n () =>\n reject(\n new Error(\n 'Build exceeded 60 seconds. Check the deck for loops or unresolved promises.',\n ),\n ),\n 60_000,\n );\n worker.once('message', (result: BuildResult) => resolveBuild(result));\n worker.once('error', reject);\n worker.once('exit', (code) =>\n reject(new Error(`Build worker exited without a result (code ${code}).`)),\n );\n });\n } finally {\n clearTimeout(timer);\n await worker.terminate();\n }\n}\nexport async function exportDeck(entry: string, output: string): Promise<BuildResult> {\n const result = await buildDeck(entry);\n await writeFile(output, result.bytes);\n return result;\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,93 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { getSlideSize, getSlides, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
5
+ import { build, transform } from "esbuild";
6
+ import { tmpdir } from "node:os";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { compile } from "@office-kit/pptx-dsl";
9
+ import { renderSlideToSvg } from "@office-kit/pptx-preview";
10
+ //#region src/build.ts
11
+ const resolvePackage = (name) => fileURLToPath(import.meta.resolve(name));
12
+ /** Evaluates trusted local TSX. This is code execution, not a sandbox. */
13
+ async function buildDeck(entry) {
14
+ const directory = await mkdtemp(join(tmpdir(), "office-pptx-"));
15
+ const output = join(directory, "deck.mjs");
16
+ try {
17
+ const result = await build({
18
+ entryPoints: [resolve(entry)],
19
+ outfile: output,
20
+ bundle: true,
21
+ platform: "node",
22
+ format: "esm",
23
+ target: "node22",
24
+ jsx: "automatic",
25
+ jsxImportSource: "@office-kit/pptx-dsl",
26
+ sourcemap: "inline",
27
+ metafile: true,
28
+ logLevel: "silent",
29
+ alias: {
30
+ "@office-kit/pptx-dsl/jsx-runtime": resolvePackage("@office-kit/pptx-dsl/jsx-runtime"),
31
+ "@office-kit/pptx-dsl/jsx-dev-runtime": resolvePackage("@office-kit/pptx-dsl/jsx-dev-runtime"),
32
+ "@office-kit/pptx-dsl": resolvePackage("@office-kit/pptx-dsl")
33
+ },
34
+ plugins: [{
35
+ name: "source-location",
36
+ setup(builder) {
37
+ builder.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async ({ path }) => {
38
+ return {
39
+ contents: (await transform(await readFile(path, "utf8"), {
40
+ loader: path.endsWith(".tsx") ? "tsx" : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? "ts" : path.endsWith(".jsx") ? "jsx" : "js",
41
+ sourcefile: path,
42
+ sourcemap: "inline",
43
+ jsx: "automatic",
44
+ jsxDev: true,
45
+ jsxImportSource: "@office-kit/pptx-dsl",
46
+ define: { "import.meta.url": JSON.stringify(pathToFileURL(path).href) }
47
+ })).code,
48
+ loader: "js"
49
+ };
50
+ });
51
+ }
52
+ }, {
53
+ name: "shared-core",
54
+ setup(builder) {
55
+ builder.onResolve({ filter: /^@office-kit\/pptx$/ }, () => ({
56
+ path: resolvePackage("@office-kit/pptx"),
57
+ external: true
58
+ }));
59
+ }
60
+ }]
61
+ });
62
+ const module = await import(pathToFileURL(output).href);
63
+ if (!module.default) throw new Error("The TSX file must default-export a Presentation.");
64
+ const presentation = await compile(module.default);
65
+ const diagnostics = validatePresentation(presentation);
66
+ const errors = diagnostics.filter((issue) => issue.severity === "error");
67
+ if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);
68
+ const bytes = await savePresentation(presentation);
69
+ const saved = await loadPresentation(bytes);
70
+ const size = getSlideSize(saved);
71
+ return {
72
+ bytes,
73
+ aspectRatio: size ? size.width / size.height : 16 / 9,
74
+ slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),
75
+ dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),
76
+ diagnostics
77
+ };
78
+ } finally {
79
+ await rm(directory, {
80
+ recursive: true,
81
+ force: true
82
+ });
83
+ }
84
+ }
85
+ //#endregion
86
+ //#region src/worker.ts
87
+ if (!parentPort) throw new Error("The build worker must run in a worker thread.");
88
+ const { entry } = workerData;
89
+ parentPort.postMessage(await buildDeck(entry));
90
+ //#endregion
91
+ export {};
92
+
93
+ //# sourceMappingURL=worker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport { mkdtemp, readFile, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { compile, type Node } from '@office-kit/pptx-dsl';\nimport {\n getSlideSize,\n getSlides,\n loadPresentation,\n savePresentation,\n validatePresentation,\n} from '@office-kit/pptx';\nimport { renderSlideToSvg } from '@office-kit/pptx-preview';\n\nconst resolvePackage = (name: string) => fileURLToPath(import.meta.resolve(name));\nexport interface BuildResult {\n bytes: Uint8Array;\n slides: string[];\n aspectRatio: number;\n dependencies: string[];\n diagnostics: ReturnType<typeof validatePresentation>;\n}\n/** Evaluates trusted local TSX. This is code execution, not a sandbox. */\nexport async function buildDeck(entry: string): Promise<BuildResult> {\n const directory = await mkdtemp(join(tmpdir(), 'office-pptx-'));\n const output = join(directory, 'deck.mjs');\n try {\n const result = await build({\n entryPoints: [resolve(entry)],\n outfile: output,\n bundle: true,\n platform: 'node',\n format: 'esm',\n target: 'node22',\n jsx: 'automatic',\n jsxImportSource: '@office-kit/pptx-dsl',\n sourcemap: 'inline',\n metafile: true,\n logLevel: 'silent',\n alias: {\n '@office-kit/pptx-dsl/jsx-runtime': resolvePackage('@office-kit/pptx-dsl/jsx-runtime'),\n '@office-kit/pptx-dsl/jsx-dev-runtime': resolvePackage(\n '@office-kit/pptx-dsl/jsx-dev-runtime',\n ),\n '@office-kit/pptx-dsl': resolvePackage('@office-kit/pptx-dsl'),\n },\n plugins: [\n {\n name: 'source-location',\n setup(builder) {\n // A bundled module lives in a temporary directory. Keep source-relative\n // asset URLs pointing at each original module, including imported files.\n builder.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async ({ path }) => {\n const source = await readFile(path, 'utf8');\n const loader = path.endsWith('.tsx')\n ? 'tsx'\n : path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.cts')\n ? 'ts'\n : path.endsWith('.jsx')\n ? 'jsx'\n : 'js';\n const transformed = await transform(source, {\n loader,\n sourcefile: path,\n sourcemap: 'inline',\n jsx: 'automatic',\n jsxDev: true,\n jsxImportSource: '@office-kit/pptx-dsl',\n define: { 'import.meta.url': JSON.stringify(pathToFileURL(path).href) },\n });\n return { contents: transformed.code, loader: 'js' };\n });\n },\n },\n {\n name: 'shared-core',\n setup(builder) {\n // Core uses symbol-backed handles; all consumers must share its instance.\n builder.onResolve({ filter: /^@office-kit\\/pptx$/ }, () => ({\n path: resolvePackage('@office-kit/pptx'),\n external: true,\n }));\n },\n },\n ],\n });\n const module: { default?: Node } = await import(pathToFileURL(output).href);\n if (!module.default) throw new Error('The TSX file must default-export a Presentation.');\n const presentation = await compile(module.default);\n const diagnostics = validatePresentation(presentation);\n const errors = diagnostics.filter((issue) => issue.severity === 'error');\n if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);\n const bytes = await savePresentation(presentation);\n // Preview serialized output too, so persistence defects are visible during authoring.\n const saved = await loadPresentation(bytes);\n const size = getSlideSize(saved);\n return {\n bytes,\n aspectRatio: size ? size.width / size.height : 16 / 9,\n slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),\n dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),\n diagnostics,\n };\n } finally {\n await rm(directory, { recursive: true, force: true });\n }\n}\n","import { parentPort, workerData } from 'node:worker_threads';\nimport { buildDeck } from './build.ts';\n\nif (!parentPort) throw new Error('The build worker must run in a worker thread.');\nconst { entry } = workerData as { entry: string };\nparentPort.postMessage(await buildDeck(entry));\n"],"mappings":";;;;;;;;;;AAeA,MAAM,kBAAkB,SAAiB,cAAc,OAAO,KAAK,QAAQ,IAAI,CAAC;;AAShF,eAAsB,UAAU,OAAqC;CACnE,MAAM,YAAY,MAAM,QAAQ,KAAK,OAAO,GAAG,cAAc,CAAC;CAC9D,MAAM,SAAS,KAAK,WAAW,UAAU;CACzC,IAAI;EACF,MAAM,SAAS,MAAM,MAAM;GACzB,aAAa,CAAC,QAAQ,KAAK,CAAC;GAC5B,SAAS;GACT,QAAQ;GACR,UAAU;GACV,QAAQ;GACR,QAAQ;GACR,KAAK;GACL,iBAAiB;GACjB,WAAW;GACX,UAAU;GACV,UAAU;GACV,OAAO;IACL,oCAAoC,eAAe,kCAAkC;IACrF,wCAAwC,eACtC,sCACF;IACA,wBAAwB,eAAe,sBAAsB;GAC/D;GACA,SAAS,CACP;IACE,MAAM;IACN,MAAM,SAAS;KAGb,QAAQ,OAAO,EAAE,QAAQ,kBAAkB,GAAG,OAAO,EAAE,WAAW;MAkBhE,OAAO;OAAE,WAAU,MATO,UAAU,MARf,SAAS,MAAM,MAAM,GAQE;QAC1C,QARa,KAAK,SAAS,MAAM,IAC/B,QACA,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,IACnE,OACA,KAAK,SAAS,MAAM,IAClB,QACA;QAGN,YAAY;QACZ,WAAW;QACX,KAAK;QACL,QAAQ;QACR,iBAAiB;QACjB,QAAQ,EAAE,mBAAmB,KAAK,UAAU,cAAc,IAAI,CAAC,CAAC,IAAI,EAAE;OACxE,CAAC,EAAA,CAC8B;OAAM,QAAQ;MAAK;KACpD,CAAC;IACH;GACF,GACA;IACE,MAAM;IACN,MAAM,SAAS;KAEb,QAAQ,UAAU,EAAE,QAAQ,sBAAsB,UAAU;MAC1D,MAAM,eAAe,kBAAkB;MACvC,UAAU;KACZ,EAAE;IACJ;GACF,CACF;EACF,CAAC;EACD,MAAM,SAA6B,MAAM,OAAO,cAAc,MAAM,CAAC,CAAC;EACtE,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,MAAM,kDAAkD;EACvF,MAAM,eAAe,MAAM,QAAQ,OAAO,OAAO;EACjD,MAAM,cAAc,qBAAqB,YAAY;EACrD,MAAM,SAAS,YAAY,QAAQ,UAAU,MAAM,aAAa,OAAO;EACvE,IAAI,OAAO,QAAQ,MAAM,IAAI,MAAM,yBAAyB,KAAK,UAAU,MAAM,GAAG;EACpF,MAAM,QAAQ,MAAM,iBAAiB,YAAY;EAEjD,MAAM,QAAQ,MAAM,iBAAiB,KAAK;EAC1C,MAAM,OAAO,aAAa,KAAK;EAC/B,OAAO;GACL;GACA,aAAa,OAAO,KAAK,QAAQ,KAAK,SAAS,KAAK;GACpD,QAAQ,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,iBAAiB,OAAO,KAAK,CAAC;GACtE,cAAc,OAAO,KAAK,OAAO,SAAS,MAAM,CAAC,CAAC,KAAK,SAAS,QAAQ,IAAI,CAAC;GAC7E;EACF;CACF,UAAU;EACR,MAAM,GAAG,WAAW;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACtD;AACF;;;ACxGA,IAAI,CAAC,YAAY,MAAM,IAAI,MAAM,+CAA+C;AAChF,MAAM,EAAE,UAAU;AAClB,WAAW,YAAY,MAAM,UAAU,KAAK,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@office-kit/pptx-dev",
3
+ "version": "0.2.0",
4
+ "description": "Build and preview PowerPoint TSX presentations locally",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/office-kit/pptx.git",
9
+ "directory": "packages/dev"
10
+ },
11
+ "bin": {
12
+ "office-pptx": "dist/cli.mjs"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.mts",
22
+ "import": "./dist/index.mjs"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "provenance": true
28
+ },
29
+ "dependencies": {
30
+ "esbuild": "^0.28.1",
31
+ "@office-kit/pptx": "^0.16.1",
32
+ "@office-kit/pptx-preview": "^0.9.5",
33
+ "@office-kit/pptx-dsl": "^0.2.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.13.2",
37
+ "tsdown": "^0.22.2",
38
+ "typescript": "^6.0.3"
39
+ },
40
+ "engines": {
41
+ "node": ">=22.18.0"
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "node --test test/*.test.mjs"
47
+ }
48
+ }