@office-kit/pptx-dev 0.2.0 → 0.2.2
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/README.md +49 -22
- package/dist/cli.mjs +81 -32
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-BA02cMY9.mjs → src-31HzWTvZ.mjs} +113 -27
- package/dist/src-31HzWTvZ.mjs.map +1 -0
- package/dist/worker.mjs +62 -74
- package/dist/worker.mjs.map +1 -1
- package/package.json +7 -5
- package/dist/src-BA02cMY9.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -4,36 +4,27 @@ Local TSX authoring tools for `@office-kit/pptx-dsl`. Build and preview an edita
|
|
|
4
4
|
PowerPoint presentation while changing its source in VSCode or Claude Code.
|
|
5
5
|
Requires Node.js 22.18 or later.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
For Claude Code, [install the office-kit skill](https://office-kit.github.io/pptx/docs/authoring)
|
|
8
|
+
and ask it to create a presentation. The skill handles setup, preview and export.
|
|
9
|
+
|
|
10
|
+
To create a project yourself:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
13
|
+
npx --yes @office-kit/pptx-dev@latest init my-slides
|
|
14
|
+
cd my-slides
|
|
15
|
+
npm install
|
|
22
16
|
npm run dev
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
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
|
|
19
|
+
Open the local URL printed by the server. Save a slide file, `theme.ts` or `deck.tsx` to rebuild. The viewer
|
|
33
20
|
has a vertical thumbnail strip and a large slide canvas. Click a thumbnail or use
|
|
34
21
|
arrow keys, Page Up/Down, Home/End to navigate. Fit/zoom and Present (Escape to
|
|
35
22
|
exit) are viewing controls; the canvas has no editing, dragging or resize handles.
|
|
36
23
|
Changes are made only in TSX, including when an AI agent edits the presentation.
|
|
24
|
+
Keep the server running throughout the edit/review loop. Saving updates only the
|
|
25
|
+
changed thumbnails and slide view, preserving zoom, scroll position and presentation
|
|
26
|
+
mode. The previous slide stays visible until its replacement is ready. Rapid edits
|
|
27
|
+
cancel obsolete evaluations; only the latest successful result is published.
|
|
37
28
|
Download PPTX exports the last successful build.
|
|
38
29
|
A syntax or runtime error is shown without discarding the last successful preview.
|
|
39
30
|
DSL evaluation errors include the TSX element's source file and line number.
|
|
@@ -54,6 +45,27 @@ presentation** task, then use **Simple Browser: Show** with the printed URL and
|
|
|
54
45
|
move it to a side editor group. No custom editor extension is needed for this
|
|
55
46
|
workflow. Preview-to-source selection is not implemented.
|
|
56
47
|
|
|
48
|
+
## Edit only what changed
|
|
49
|
+
|
|
50
|
+
New projects separate slide order, content and shared styling:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
deck.tsx # Slide imports and order
|
|
54
|
+
slides/cover.tsx # One slide per descriptively named file
|
|
55
|
+
theme.ts # Shared design values
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For example, a headline revision changes only the text in the corresponding
|
|
59
|
+
slide file. A deck-wide palette change belongs in `theme.ts`; a one-slide color
|
|
60
|
+
exception belongs in that slide's props. Imported files are watched, so keep the
|
|
61
|
+
preview running while editing. Existing single-file decks remain supported and
|
|
62
|
+
do not need migration for a small revision.
|
|
63
|
+
|
|
64
|
+
Use focused code patches for revisions. Review affected slides while iterating;
|
|
65
|
+
check types, export and review the whole deck before delivery. The build still
|
|
66
|
+
evaluates the whole presentation; file splitting reduces authoring scope, not
|
|
67
|
+
the amount of PPTX evaluation.
|
|
68
|
+
|
|
57
69
|
## Commands
|
|
58
70
|
|
|
59
71
|
- `office-pptx init <new-directory>` creates a TSX project without overwriting an existing directory.
|
|
@@ -67,7 +79,8 @@ assets with `readFile(new URL('./asset.png', import.meta.url))`; the compiler
|
|
|
67
79
|
preserves the original URL of each bundled source module.
|
|
68
80
|
|
|
69
81
|
TSX runs as trusted local Node code, including imports and Raw callbacks.
|
|
70
|
-
|
|
82
|
+
The watch server preloads libraries in the next worker between edits. Each build
|
|
83
|
+
still evaluates the complete TSX in a fresh worker, with a 60-second limit, so module state
|
|
71
84
|
cannot accumulate across updates. Workers are not a security sandbox. The
|
|
72
85
|
preview server binds to `127.0.0.1` and rejects unexpected Host headers.
|
|
73
86
|
|
|
@@ -87,3 +100,17 @@ node packages/dev/dist/cli.mjs dev packages/dsl/examples/review.tsx
|
|
|
87
100
|
node packages/dev/dist/cli.mjs build packages/dsl/examples/review.tsx --out review.pptx
|
|
88
101
|
pnpm --filter @office-kit/pptx-dev test
|
|
89
102
|
```
|
|
103
|
+
|
|
104
|
+
Browser regression tests cover partial updates, viewport preservation, errors,
|
|
105
|
+
reconnection and presentation mode, and report save-to-visible timings for a
|
|
106
|
+
50-slide text deck:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
pnpm --filter @office-kit/pptx-dev exec playwright install chromium
|
|
110
|
+
pnpm --filter @office-kit/pptx-dev test:browser
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
To use an installed Chrome instead, set `PLAYWRIGHT_CHANNEL=chrome`. Timings depend
|
|
114
|
+
on the deck and machine; complex templates and charts still incur full generation
|
|
115
|
+
and serialization costs. SVG updates are transferred as deltas, with a complete
|
|
116
|
+
snapshot after reconnecting or falling behind.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as initProject, n as exportDeck, r as inspectTemplate
|
|
2
|
+
import { a as createDeckBuilder, i as initProject, n as exportDeck, r as inspectTemplate } from "./src-31HzWTvZ.mjs";
|
|
3
3
|
import { dirname, resolve, sep } from "node:path";
|
|
4
|
-
import { createServer } from "node:http";
|
|
5
4
|
import { watch } from "node:fs";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
6
|
//#region src/page.ts
|
|
7
7
|
const page = `<!doctype html>
|
|
8
8
|
<html lang="en">
|
|
@@ -29,8 +29,8 @@ main{min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
|
29
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
30
|
#error[hidden]{display:none}
|
|
31
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}
|
|
32
|
+
#slide{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}
|
|
33
|
+
#slide iframe{position:absolute;inset:0;display:block;width:100%;height:100%;border:0;pointer-events:none}
|
|
34
34
|
#empty{margin:auto;color:#737d8e}
|
|
35
35
|
footer{display:flex;align-items:center;gap:14px;padding:0 16px;background:#fff;border-top:1px solid #d4d9e2;font-size:12px}
|
|
36
36
|
#count{min-width:90px}footer .hint{flex:1;color:#737d8e}footer button{padding:3px 10px}footer select{padding:3px 8px}
|
|
@@ -53,6 +53,7 @@ body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
|
|
|
53
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
54
|
<script>
|
|
55
55
|
let state={slides:[],error:null,aspectRatio:16/9},index=0,urls=[],presenting=false;
|
|
56
|
+
let displayedSvg, pendingFrame;
|
|
56
57
|
const byId=id=>document.getElementById(id);
|
|
57
58
|
const stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');
|
|
58
59
|
function resize(){
|
|
@@ -65,7 +66,7 @@ function resize(){
|
|
|
65
66
|
const slideWidth=presenting||zoom==='fit'?Math.min(width,height*ratio):1280*Number(zoom);
|
|
66
67
|
slide.style.width=slideWidth+'px';slide.style.height=slideWidth/ratio+'px';
|
|
67
68
|
}
|
|
68
|
-
function selectSlide(next,focusThumbnail=false){
|
|
69
|
+
function selectSlide(next,focusThumbnail=false,reveal=true){
|
|
69
70
|
index=Math.max(0,Math.min(next,state.slides.length-1));
|
|
70
71
|
const count=state.slides.length?'Slide '+(index+1)+' of '+state.slides.length:'No slides';
|
|
71
72
|
byId('count').textContent=count;byId('present-count').textContent=count;
|
|
@@ -74,38 +75,59 @@ function selectSlide(next,focusThumbnail=false){
|
|
|
74
75
|
byId('present').disabled=!state.slides.length;
|
|
75
76
|
byId('zoom').disabled=!state.slides.length;
|
|
76
77
|
slide.hidden=!state.slides.length;byId('empty').hidden=!!state.slides.length;
|
|
77
|
-
|
|
78
|
-
if(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const svg=state.slides[index];
|
|
79
|
+
if(svg!==displayedSvg){
|
|
80
|
+
displayedSvg=svg;
|
|
81
|
+
if(pendingFrame){pendingFrame.remove();pendingFrame=null;}
|
|
82
|
+
if(svg){
|
|
83
|
+
const frame=document.createElement('iframe');frame.sandbox='';frame.tabIndex=-1;
|
|
84
|
+
frame.title='Slide '+(index+1);frame.style.visibility='hidden';
|
|
85
|
+
pendingFrame=frame;
|
|
86
|
+
frame.onload=()=>{
|
|
87
|
+
if(pendingFrame!==frame)return;
|
|
88
|
+
for(const child of Array.from(slide.children))if(child!==frame)child.remove();
|
|
89
|
+
frame.style.visibility='visible';pendingFrame=null;
|
|
90
|
+
};
|
|
91
|
+
frame.srcdoc='<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}svg{display:block;width:100%;height:100%}</style>'+svg;
|
|
92
|
+
slide.append(frame);
|
|
93
|
+
}else slide.replaceChildren();
|
|
82
94
|
}
|
|
95
|
+
for(const frame of slide.children)frame.title='Slide '+(index+1);
|
|
83
96
|
for(const [position,item] of Array.from(thumbnails.children).entries()){
|
|
84
97
|
const button=item.firstElementChild,selected=position===index;
|
|
85
98
|
button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;
|
|
86
|
-
if(selected){button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}
|
|
99
|
+
if(selected){if(reveal)button.scrollIntoView({block:'nearest'});if(focusThumbnail)button.focus({preventScroll:true});}
|
|
87
100
|
}
|
|
88
101
|
resize();
|
|
89
102
|
}
|
|
90
103
|
function update(updated){
|
|
91
104
|
const focusedThumbnail=thumbnails.contains(document.activeElement);
|
|
92
|
-
const
|
|
105
|
+
const previous=state;
|
|
93
106
|
state=updated;
|
|
94
|
-
byId('status').textContent=state.error?'Build failed · showing last successful output':state.slides.length+' slides · Live';
|
|
107
|
+
byId('status').textContent=state.error?'Build failed · showing last successful output':state.building?'Updating…':state.slides.length+' slides · Live';
|
|
95
108
|
byId('error').textContent=state.error||'';byId('error').hidden=!state.error;
|
|
96
109
|
document.documentElement.style.setProperty('--slide-ratio',String(state.aspectRatio));
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
for(let i=state.slides.length;i<urls.length;i++){
|
|
111
|
+
URL.revokeObjectURL(urls[i]);thumbnails.lastElementChild.remove();
|
|
112
|
+
}
|
|
113
|
+
urls.length=state.slides.length;
|
|
114
|
+
state.slides.forEach((svg,i)=>{
|
|
115
|
+
if(svg===previous.slides[i])return;
|
|
116
|
+
const oldUrl=urls[i];
|
|
117
|
+
urls[i]=URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'}));
|
|
118
|
+
let item=thumbnails.children[i];
|
|
119
|
+
if(!item){
|
|
120
|
+
item=document.createElement('li');
|
|
121
|
+
const button=document.createElement('button'),number=document.createElement('span'),image=document.createElement('img');
|
|
102
122
|
button.className='thumbnail';button.setAttribute('aria-label','Slide '+(i+1));button.onclick=()=>selectSlide(i,true);
|
|
103
123
|
number.className='slide-number';number.textContent=String(i+1);
|
|
104
|
-
image.
|
|
105
|
-
button.append(number,image);item.append(button);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
image.alt='';image.draggable=false;image.loading='lazy';
|
|
125
|
+
button.append(number,image);item.append(button);thumbnails.append(item);
|
|
126
|
+
}
|
|
127
|
+
item.querySelector('img').src=urls[i];
|
|
128
|
+
if(oldUrl)URL.revokeObjectURL(oldUrl);
|
|
129
|
+
});
|
|
130
|
+
selectSlide(index,focusedThumbnail,false);
|
|
109
131
|
if(!state.slides.length&&presenting)void exitPresentation();
|
|
110
132
|
}
|
|
111
133
|
function setPresenting(value){
|
|
@@ -146,10 +168,10 @@ new ResizeObserver(resize).observe(stage);
|
|
|
146
168
|
let refreshId=0;
|
|
147
169
|
async function refresh(){
|
|
148
170
|
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);}
|
|
171
|
+
try{const response=await fetch('/state'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}
|
|
150
172
|
catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}
|
|
151
173
|
}
|
|
152
|
-
const events=new EventSource('/events');events.onmessage=refresh;events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
|
|
174
|
+
const events=new EventSource('/events');events.onmessage=event=>{if(event.data==='ready')delete state.revision;void refresh();};events.onerror=()=>{byId('status').textContent='Reconnecting…'};refresh();
|
|
153
175
|
<\/script></html>`;
|
|
154
176
|
//#endregion
|
|
155
177
|
//#region src/server.ts
|
|
@@ -157,6 +179,9 @@ async function serveDeck(entry, port = 4173) {
|
|
|
157
179
|
let latest;
|
|
158
180
|
let error = null;
|
|
159
181
|
let building = false;
|
|
182
|
+
let revision = 0;
|
|
183
|
+
let generation = 0;
|
|
184
|
+
let patch = {};
|
|
160
185
|
let pending = false;
|
|
161
186
|
let closed = false;
|
|
162
187
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -172,10 +197,17 @@ async function serveDeck(entry, port = 4173) {
|
|
|
172
197
|
response.write("data: ready\n\n");
|
|
173
198
|
clients.add(response);
|
|
174
199
|
request.on("close", () => clients.delete(response));
|
|
175
|
-
} else if (request.url === "/state") {
|
|
200
|
+
} else if (request.url === "/state" || request.url?.startsWith("/state?")) {
|
|
201
|
+
const since = new URL(request.url, "http://localhost").searchParams.get("since");
|
|
202
|
+
const incremental = since !== null && (since === String(revision) || since === String(revision - 1));
|
|
176
203
|
response.writeHead(200, { "Content-Type": "application/json" });
|
|
177
204
|
response.end(JSON.stringify({
|
|
178
|
-
|
|
205
|
+
revision,
|
|
206
|
+
building,
|
|
207
|
+
...incremental ? {
|
|
208
|
+
changes: since === String(revision) ? {} : patch,
|
|
209
|
+
count: latest?.slides.length ?? 0
|
|
210
|
+
} : { slides: latest?.slides ?? [] },
|
|
179
211
|
aspectRatio: latest?.aspectRatio ?? 16 / 9,
|
|
180
212
|
error,
|
|
181
213
|
diagnostics: latest?.diagnostics ?? []
|
|
@@ -198,15 +230,25 @@ async function serveDeck(entry, port = 4173) {
|
|
|
198
230
|
return;
|
|
199
231
|
}
|
|
200
232
|
building = true;
|
|
233
|
+
const started = generation;
|
|
234
|
+
for (const client of clients) client.write("data: building\n\n");
|
|
201
235
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
236
|
+
const result = await builder.build();
|
|
237
|
+
if (started === generation && !closed) {
|
|
238
|
+
patch = {};
|
|
239
|
+
result.slides.forEach((svg, index) => {
|
|
240
|
+
if (svg !== latest?.slides[index]) patch[index] = svg;
|
|
241
|
+
});
|
|
242
|
+
latest = result;
|
|
243
|
+
revision++;
|
|
244
|
+
error = null;
|
|
245
|
+
}
|
|
204
246
|
} catch (cause) {
|
|
205
|
-
error = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
|
|
247
|
+
if (started === generation) error = cause instanceof Error ? cause.stack ?? cause.message : String(cause);
|
|
206
248
|
} finally {
|
|
207
249
|
building = false;
|
|
208
250
|
for (const client of clients) client.write("data: updated\n\n");
|
|
209
|
-
if (pending) {
|
|
251
|
+
if (pending && !timer) {
|
|
210
252
|
pending = false;
|
|
211
253
|
rebuild();
|
|
212
254
|
}
|
|
@@ -221,11 +263,16 @@ async function serveDeck(entry, port = 4173) {
|
|
|
221
263
|
".office-kit"
|
|
222
264
|
].includes(part))) return;
|
|
223
265
|
if (!/\.([cm]?[jt]sx?|json|pptx|png|jpe?g|gif|bmp|tiff?|emf|wmf|svg)$/i.test(filename)) return;
|
|
266
|
+
generation++;
|
|
267
|
+
builder.cancel();
|
|
224
268
|
clearTimeout(timer);
|
|
225
269
|
timer = setTimeout(() => {
|
|
270
|
+
timer = void 0;
|
|
271
|
+
pending = false;
|
|
226
272
|
rebuild();
|
|
227
|
-
},
|
|
273
|
+
}, 30);
|
|
228
274
|
});
|
|
275
|
+
const builder = createDeckBuilder(entry);
|
|
229
276
|
let actualPort = port;
|
|
230
277
|
try {
|
|
231
278
|
await new Promise((resolveListen, reject) => {
|
|
@@ -237,6 +284,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
237
284
|
});
|
|
238
285
|
} catch (cause) {
|
|
239
286
|
watcher.close();
|
|
287
|
+
await builder.close();
|
|
240
288
|
throw cause;
|
|
241
289
|
}
|
|
242
290
|
const address = server.address();
|
|
@@ -248,6 +296,7 @@ async function serveDeck(entry, port = 4173) {
|
|
|
248
296
|
closed = true;
|
|
249
297
|
clearTimeout(timer);
|
|
250
298
|
watcher.close();
|
|
299
|
+
await builder.close();
|
|
251
300
|
for (const client of clients) client.end();
|
|
252
301
|
await new Promise((done, reject) => server.close((cause) => cause ? reject(cause) : done()));
|
|
253
302
|
}
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +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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"}
|
|
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{position:relative;flex:none;margin:auto;background:white;box-shadow:0 3px 24px #19212d20;overflow:hidden}\n#slide iframe{position:absolute;inset:0;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;\nlet displayedSvg, pendingFrame;\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,reveal=true){\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 const svg=state.slides[index];\n if(svg!==displayedSvg){\n displayedSvg=svg;\n if(pendingFrame){pendingFrame.remove();pendingFrame=null;}\n if(svg){\n const frame=document.createElement('iframe');frame.sandbox='';frame.tabIndex=-1;\n frame.title='Slide '+(index+1);frame.style.visibility='hidden';\n pendingFrame=frame;\n frame.onload=()=>{\n if(pendingFrame!==frame)return;\n for(const child of Array.from(slide.children))if(child!==frame)child.remove();\n frame.style.visibility='visible';pendingFrame=null;\n };\n frame.srcdoc='<style>html,body{margin:0;width:100%;height:100%;overflow:hidden}svg{display:block;width:100%;height:100%}</style>'+svg;\n slide.append(frame);\n }else slide.replaceChildren();\n }\n for(const frame of slide.children)frame.title='Slide '+(index+1);\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){if(reveal)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 previous=state;\n state=updated;\n byId('status').textContent=state.error?'Build failed · showing last successful output':state.building?'Updating…':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 for(let i=state.slides.length;i<urls.length;i++){\n URL.revokeObjectURL(urls[i]);thumbnails.lastElementChild.remove();\n }\n urls.length=state.slides.length;\n state.slides.forEach((svg,i)=>{\n if(svg===previous.slides[i])return;\n const oldUrl=urls[i];\n urls[i]=URL.createObjectURL(new Blob([svg],{type:'image/svg+xml'}));\n let item=thumbnails.children[i];\n if(!item){\n item=document.createElement('li');\n const 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.alt='';image.draggable=false;image.loading='lazy';\n button.append(number,image);item.append(button);thumbnails.append(item);\n }\n item.querySelector('img').src=urls[i];\n if(oldUrl)URL.revokeObjectURL(oldUrl);\n });\n selectSlide(index,focusedThumbnail,false);\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'+(state.revision===undefined?'':'?since='+state.revision));if(!response.ok)throw new Error('Preview unavailable');const updated=await response.json();if(id===refreshId){if(updated.changes){updated.slides=state.slides.slice(0,updated.count);updated.slides.length=updated.count;for(const [position,svg] of Object.entries(updated.changes))updated.slides[Number(position)]=svg;}update(updated);}}\n catch{if(id===refreshId)byId('status').textContent='Reconnecting…';}\n}\nconst events=new EventSource('/events');events.onmessage=event=>{if(event.data==='ready')delete state.revision;void 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 type { BuildResult } from './build.ts';\nimport { createDeckBuilder } from './build-runner.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 revision = 0;\n let generation = 0;\n let patch: Record<number, string> = {};\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' || request.url?.startsWith('/state?')) {\n const since = new URL(request.url, 'http://localhost').searchParams.get('since');\n const incremental =\n since !== null && (since === String(revision) || since === String(revision - 1));\n response.writeHead(200, { 'Content-Type': 'application/json' });\n response.end(\n JSON.stringify({\n revision,\n building,\n ...(incremental\n ? {\n changes: since === String(revision) ? {} : patch,\n count: latest?.slides.length ?? 0,\n }\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 const started = generation;\n for (const client of clients) client.write('data: building\\n\\n');\n try {\n const result = await builder.build();\n if (started === generation && !closed) {\n patch = {};\n result.slides.forEach((svg, index) => {\n if (svg !== latest?.slides[index]) patch[index] = svg;\n });\n latest = result;\n revision++;\n error = null;\n }\n } catch (cause) {\n if (started === generation)\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 && !timer) {\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 generation++;\n void builder.cancel();\n clearTimeout(timer);\n timer = setTimeout(() => {\n timer = undefined;\n pending = false;\n void rebuild();\n }, 30);\n });\n const builder = createDeckBuilder(entry);\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 await builder.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 await builder.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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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as initProject, n as exportDeck, r as inspectTemplate, t as buildDeck } from "./src-
|
|
1
|
+
import { i as initProject, n as exportDeck, r as inspectTemplate, t as buildDeck } from "./src-31HzWTvZ.mjs";
|
|
2
2
|
export { buildDeck, exportDeck, initProject, inspectTemplate };
|
|
@@ -1,27 +1,113 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
1
2
|
import { Worker } from "node:worker_threads";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { mkdtempSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
import { getShapeId, getShapeKind, getShapeName, getShapePlaceholderIdx, getShapePlaceholderType, getSlideLayoutName, getSlideLayoutPartName, getSlideLayoutType, getSlideLayouts, getSlidePartName, getSlideShapes, getSlideTitle, getSlides, loadPresentation } from "@office-kit/pptx";
|
|
7
|
+
//#region src/build-runner.ts
|
|
8
|
+
const BUILD_TIMEOUT_MS = 6e4;
|
|
9
|
+
function prepareWorker(entry) {
|
|
10
|
+
const directory = mkdtempSync(join(tmpdir(), "office-pptx-"));
|
|
11
|
+
const worker = new Worker(new URL("./worker.mjs", import.meta.url), {
|
|
12
|
+
workerData: {
|
|
13
|
+
entry: resolve(entry),
|
|
14
|
+
directory
|
|
15
|
+
},
|
|
16
|
+
execArgv: ["--enable-source-maps"]
|
|
17
|
+
});
|
|
18
|
+
const result = new Promise((resolveBuild, reject) => {
|
|
19
|
+
worker.once("message", resolveBuild);
|
|
20
|
+
worker.once("error", reject);
|
|
21
|
+
worker.once("exit", (code) => reject(/* @__PURE__ */ new Error(`Build worker exited without a result (code ${code}).`)));
|
|
22
|
+
});
|
|
23
|
+
result.catch(() => {});
|
|
24
|
+
return {
|
|
25
|
+
worker,
|
|
26
|
+
result,
|
|
27
|
+
directory
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Internal watch session. Preload libraries, but never reuse a user module cache. */
|
|
31
|
+
function createDeckBuilder(entry, keepWarm = true) {
|
|
32
|
+
let prepared = prepareWorker(entry);
|
|
33
|
+
let closed = false;
|
|
34
|
+
let active = false;
|
|
35
|
+
return {
|
|
36
|
+
async build() {
|
|
37
|
+
if (closed || active) throw new Error("Deck builder is closed or already building.");
|
|
38
|
+
active = true;
|
|
39
|
+
const current = prepared ?? prepareWorker(entry);
|
|
40
|
+
prepared = current;
|
|
41
|
+
let timer;
|
|
42
|
+
try {
|
|
43
|
+
current.worker.postMessage("build");
|
|
44
|
+
return await Promise.race([current.result, new Promise((_, reject) => {
|
|
45
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Build exceeded 60 seconds. Check the deck for loops or unresolved promises.")), BUILD_TIMEOUT_MS);
|
|
46
|
+
})]);
|
|
47
|
+
} finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
await current.worker.terminate();
|
|
50
|
+
await rm(current.directory, {
|
|
51
|
+
recursive: true,
|
|
52
|
+
force: true
|
|
53
|
+
});
|
|
54
|
+
active = false;
|
|
55
|
+
prepared = !closed && keepWarm ? prepareWorker(entry) : void 0;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
async cancel() {
|
|
59
|
+
if (active) await prepared?.worker.terminate();
|
|
60
|
+
},
|
|
61
|
+
async close() {
|
|
62
|
+
closed = true;
|
|
63
|
+
const current = prepared;
|
|
64
|
+
await current?.worker.terminate();
|
|
65
|
+
if (current) await rm(current.directory, {
|
|
66
|
+
recursive: true,
|
|
67
|
+
force: true
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
5
73
|
//#region src/init.ts
|
|
6
|
-
const starter = `import { Presentation
|
|
74
|
+
const starter = `import { Presentation } from '@office-kit/pptx-dsl';
|
|
75
|
+
import { Cover } from './slides/cover.tsx';
|
|
7
76
|
|
|
8
77
|
export default (
|
|
9
78
|
<Presentation>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
79
|
+
<Cover />
|
|
80
|
+
</Presentation>
|
|
81
|
+
);
|
|
82
|
+
`;
|
|
83
|
+
const theme = `export const theme = {
|
|
84
|
+
background: '#15171C',
|
|
85
|
+
accent: '#E5481F',
|
|
86
|
+
text: '#FFFFFF',
|
|
87
|
+
muted: '#B4B9C4',
|
|
88
|
+
} as const;
|
|
89
|
+
`;
|
|
90
|
+
const cover = `import { Slide, Text, Shape } from '@office-kit/pptx-dsl';
|
|
91
|
+
import { theme } from '../theme.ts';
|
|
92
|
+
|
|
93
|
+
export function Cover() {
|
|
94
|
+
return (
|
|
95
|
+
<Slide background={theme.background}>
|
|
96
|
+
<Shape preset="rect" x={0.9} y={2.55} width={0.14} height={1.75} fill={theme.accent} stroke={false} />
|
|
97
|
+
<Text x={1.25} y={2.4} width={10} height={1.1} size={48} bold color={theme.text}>
|
|
13
98
|
Your next presentation
|
|
14
99
|
</Text>
|
|
15
|
-
<Text x={1.25} y={3.55} width={10} height={0.6} size={22} color=
|
|
100
|
+
<Text x={1.25} y={3.55} width={10} height={0.6} size={22} color={theme.muted}>
|
|
16
101
|
Edit this TSX and save to update the preview.
|
|
17
102
|
</Text>
|
|
18
103
|
</Slide>
|
|
19
|
-
|
|
20
|
-
|
|
104
|
+
);
|
|
105
|
+
}
|
|
21
106
|
`;
|
|
22
107
|
const guide = `# Writing this presentation
|
|
23
108
|
|
|
24
|
-
|
|
109
|
+
Use deck.tsx for slide order, slides/*.tsx for individual slides, and theme.ts
|
|
110
|
+
for shared design values. The JSX runtime is
|
|
25
111
|
@office-kit/pptx-dsl, with no React or Vue dependency.
|
|
26
112
|
|
|
27
113
|
- Run npm run dev, then open its local URL to preview. Saving source files
|
|
@@ -29,8 +115,16 @@ Edit deck.tsx and local TypeScript components. The JSX runtime is
|
|
|
29
115
|
- The preview is view-only: use the vertical thumbnails to select slides, zoom
|
|
30
116
|
to inspect details, and Present to view full-screen (Escape exits). Make all
|
|
31
117
|
content and layout changes in TSX; there are no canvas editing controls.
|
|
32
|
-
-
|
|
33
|
-
|
|
118
|
+
- For a local revision, locate the slide through deck.tsx or a text search and
|
|
119
|
+
read only the relevant source and dependencies. Patch the requested text, data
|
|
120
|
+
or props; do not regenerate the deck or reformat unrelated code. Keep descriptive
|
|
121
|
+
filenames stable when reordering slides. Preserve existing project structures.
|
|
122
|
+
- Change shared theme values/components only for changes intended for all their
|
|
123
|
+
consumers; use a local prop override for a one-slide exception.
|
|
124
|
+
- Keep the preview running during revisions and inspect affected slides after
|
|
125
|
+
saving. Review all affected consumers for shared changes. Do not run a separate
|
|
126
|
+
export for every intermediate edit. Before delivery, run npm run check and
|
|
127
|
+
npm run build to export deck.pptx, then review the whole deck.
|
|
34
128
|
- Coordinates and dimensions are inches; font sizes and stroke widths are points.
|
|
35
129
|
The default 16:9 canvas is 13.333 × 7.5 inches.
|
|
36
130
|
- Use native Text, Shape, Image, Table and Chart elements. Use JS functions,
|
|
@@ -103,6 +197,8 @@ async function initProject(directory) {
|
|
|
103
197
|
exclude: ["node_modules"]
|
|
104
198
|
}, null, 2) + "\n",
|
|
105
199
|
"deck.tsx": starter,
|
|
200
|
+
"theme.ts": theme,
|
|
201
|
+
"slides/cover.tsx": cover,
|
|
106
202
|
"CLAUDE.md": guide,
|
|
107
203
|
".gitignore": "node_modules/\ndeck.pptx\n",
|
|
108
204
|
".vscode/tasks.json": JSON.stringify({
|
|
@@ -141,7 +237,7 @@ async function initProject(directory) {
|
|
|
141
237
|
]
|
|
142
238
|
}, null, 2) + "\n"
|
|
143
239
|
};
|
|
144
|
-
await mkdir(join(root,
|
|
240
|
+
await Promise.all([".vscode", "slides"].map((directory) => mkdir(join(root, directory))));
|
|
145
241
|
await Promise.all(Object.entries(files).map(([name, content]) => writeFile(join(root, name), content, { flag: "wx" })));
|
|
146
242
|
return root;
|
|
147
243
|
}
|
|
@@ -176,21 +272,11 @@ async function inspectTemplate(file) {
|
|
|
176
272
|
//#region src/index.ts
|
|
177
273
|
/** Each evaluation gets a fresh module cache and releases it when finished. */
|
|
178
274
|
async function buildDeck(entry) {
|
|
179
|
-
const
|
|
180
|
-
workerData: { entry: resolve(entry) },
|
|
181
|
-
execArgv: ["--enable-source-maps"]
|
|
182
|
-
});
|
|
183
|
-
let timer;
|
|
275
|
+
const builder = createDeckBuilder(entry, false);
|
|
184
276
|
try {
|
|
185
|
-
return await
|
|
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
|
-
});
|
|
277
|
+
return await builder.build();
|
|
191
278
|
} finally {
|
|
192
|
-
|
|
193
|
-
await worker.terminate();
|
|
279
|
+
await builder.close();
|
|
194
280
|
}
|
|
195
281
|
}
|
|
196
282
|
async function exportDeck(entry, output) {
|
|
@@ -199,6 +285,6 @@ async function exportDeck(entry, output) {
|
|
|
199
285
|
return result;
|
|
200
286
|
}
|
|
201
287
|
//#endregion
|
|
202
|
-
export { initProject as i, exportDeck as n, inspectTemplate as r, buildDeck as t };
|
|
288
|
+
export { createDeckBuilder as a, initProject as i, exportDeck as n, inspectTemplate as r, buildDeck as t };
|
|
203
289
|
|
|
204
|
-
//# sourceMappingURL=src-
|
|
290
|
+
//# sourceMappingURL=src-31HzWTvZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-31HzWTvZ.mjs","names":[],"sources":["../src/build-runner.ts","../src/init.ts","../src/inspect.ts","../src/index.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\nimport { resolve, join } from 'node:path';\nimport { mkdtempSync } from 'node:fs';\nimport { rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport type { BuildResult } from './build.ts';\n\nconst BUILD_TIMEOUT_MS = 60_000;\n\nfunction prepareWorker(entry: string) {\n const directory = mkdtempSync(join(tmpdir(), 'office-pptx-'));\n const worker = new Worker(new URL('./worker.mjs', import.meta.url), {\n workerData: { entry: resolve(entry), directory },\n execArgv: ['--enable-source-maps'],\n });\n const result = new Promise<BuildResult>((resolveBuild, reject) => {\n worker.once('message', resolveBuild);\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 // Startup may fail before the next edit asks to use this worker.\n void result.catch(() => {});\n return { worker, result, directory };\n}\n\n/** Internal watch session. Preload libraries, but never reuse a user module cache. */\nexport function createDeckBuilder(entry: string, keepWarm = true) {\n let prepared: ReturnType<typeof prepareWorker> | undefined = prepareWorker(entry);\n let closed = false;\n let active = false;\n return {\n async build(): Promise<BuildResult> {\n if (closed || active) throw new Error('Deck builder is closed or already building.');\n active = true;\n const current = prepared ?? prepareWorker(entry);\n prepared = current;\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n current.worker.postMessage('build');\n return await Promise.race([\n current.result,\n new Promise<never>((_, 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 BUILD_TIMEOUT_MS,\n );\n }),\n ]);\n } finally {\n clearTimeout(timer);\n await current.worker.terminate();\n await rm(current.directory, { recursive: true, force: true });\n active = false;\n prepared = !closed && keepWarm ? prepareWorker(entry) : undefined;\n }\n },\n async cancel() {\n if (active) await prepared?.worker.terminate();\n },\n async close() {\n closed = true;\n const current = prepared;\n await current?.worker.terminate();\n if (current) await rm(current.directory, { recursive: true, force: true });\n },\n };\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nconst starter = `import { Presentation } from '@office-kit/pptx-dsl';\nimport { Cover } from './slides/cover.tsx';\n\nexport default (\n <Presentation>\n <Cover />\n </Presentation>\n);\n`;\n\nconst theme = `export const theme = {\n background: '#15171C',\n accent: '#E5481F',\n text: '#FFFFFF',\n muted: '#B4B9C4',\n} as const;\n`;\n\nconst cover = `import { Slide, Text, Shape } from '@office-kit/pptx-dsl';\nimport { theme } from '../theme.ts';\n\nexport function Cover() {\n return (\n <Slide background={theme.background}>\n <Shape preset=\"rect\" x={0.9} y={2.55} width={0.14} height={1.75} fill={theme.accent} stroke={false} />\n <Text x={1.25} y={2.4} width={10} height={1.1} size={48} bold color={theme.text}>\n Your next presentation\n </Text>\n <Text x={1.25} y={3.55} width={10} height={0.6} size={22} color={theme.muted}>\n Edit this TSX and save to update the preview.\n </Text>\n </Slide>\n );\n}\n`;\n\nconst guide = `# Writing this presentation\n\nUse deck.tsx for slide order, slides/*.tsx for individual slides, and theme.ts\nfor shared design values. 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- For a local revision, locate the slide through deck.tsx or a text search and\n read only the relevant source and dependencies. Patch the requested text, data\n or props; do not regenerate the deck or reformat unrelated code. Keep descriptive\n filenames stable when reordering slides. Preserve existing project structures.\n- Change shared theme values/components only for changes intended for all their\n consumers; use a local prop override for a one-slide exception.\n- Keep the preview running during revisions and inspect affected slides after\n saving. Review all affected consumers for shared changes. Do not run a separate\n export for every intermediate edit. Before delivery, run npm run check and\n npm run build to export deck.pptx, then review the whole deck.\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 'theme.ts': theme,\n 'slides/cover.tsx': cover,\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 Promise.all(['.vscode', 'slides'].map((directory) => mkdir(join(root, directory))));\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 { writeFile } from 'node:fs/promises';\nimport { createDeckBuilder } from './build-runner.ts';\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 builder = createDeckBuilder(entry, false);\n try {\n return await builder.build();\n } finally {\n await builder.close();\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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"}
|
package/dist/worker.mjs
CHANGED
|
@@ -1,92 +1,80 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { getSlideSize, getSlides, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
|
|
5
|
+
import { once } from "node:events";
|
|
5
6
|
import { build, transform } from "esbuild";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
import { compile } from "@office-kit/pptx-dsl";
|
|
9
9
|
import { renderSlideToSvg } from "@office-kit/pptx-preview";
|
|
10
10
|
//#region src/build.ts
|
|
11
11
|
const resolvePackage = (name) => fileURLToPath(import.meta.resolve(name));
|
|
12
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-"));
|
|
13
|
+
async function buildDeck(entry, directory) {
|
|
15
14
|
const output = join(directory, "deck.mjs");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
15
|
+
const result = await build({
|
|
16
|
+
entryPoints: [resolve(entry)],
|
|
17
|
+
outfile: output,
|
|
18
|
+
bundle: true,
|
|
19
|
+
platform: "node",
|
|
20
|
+
format: "esm",
|
|
21
|
+
target: "node22",
|
|
22
|
+
jsx: "automatic",
|
|
23
|
+
jsxImportSource: "@office-kit/pptx-dsl",
|
|
24
|
+
sourcemap: "inline",
|
|
25
|
+
metafile: true,
|
|
26
|
+
logLevel: "silent",
|
|
27
|
+
plugins: [{
|
|
28
|
+
name: "source-location",
|
|
29
|
+
setup(builder) {
|
|
30
|
+
builder.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async ({ path }) => {
|
|
31
|
+
return {
|
|
32
|
+
contents: (await transform(await readFile(path, "utf8"), {
|
|
33
|
+
loader: path.endsWith(".tsx") ? "tsx" : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? "ts" : path.endsWith(".jsx") ? "jsx" : "js",
|
|
34
|
+
sourcefile: path,
|
|
35
|
+
sourcemap: "inline",
|
|
36
|
+
jsx: "automatic",
|
|
37
|
+
jsxDev: true,
|
|
38
|
+
jsxImportSource: "@office-kit/pptx-dsl",
|
|
39
|
+
define: { "import.meta.url": JSON.stringify(pathToFileURL(path).href) }
|
|
40
|
+
})).code,
|
|
41
|
+
loader: "js"
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}, {
|
|
46
|
+
name: "shared-office-kit",
|
|
47
|
+
setup(builder) {
|
|
48
|
+
builder.onResolve({ filter: /^@office-kit\/pptx(?:-dsl)?(?:\/.*)?$/ }, ({ path }) => ({
|
|
49
|
+
path: resolvePackage(path),
|
|
50
|
+
external: true
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
}]
|
|
54
|
+
});
|
|
55
|
+
const module = await import(pathToFileURL(output).href);
|
|
56
|
+
if (!module.default) throw new Error("The TSX file must default-export a Presentation.");
|
|
57
|
+
const presentation = await compile(module.default);
|
|
58
|
+
const diagnostics = validatePresentation(presentation);
|
|
59
|
+
const errors = diagnostics.filter((issue) => issue.severity === "error");
|
|
60
|
+
if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);
|
|
61
|
+
const bytes = await savePresentation(presentation);
|
|
62
|
+
const saved = await loadPresentation(bytes);
|
|
63
|
+
const size = getSlideSize(saved);
|
|
64
|
+
return {
|
|
65
|
+
bytes,
|
|
66
|
+
aspectRatio: size ? size.width / size.height : 16 / 9,
|
|
67
|
+
slides: getSlides(saved).map((slide) => renderSlideToSvg(saved, slide)),
|
|
68
|
+
dependencies: Object.keys(result.metafile.inputs).map((path) => resolve(path)),
|
|
69
|
+
diagnostics
|
|
70
|
+
};
|
|
84
71
|
}
|
|
85
72
|
//#endregion
|
|
86
73
|
//#region src/worker.ts
|
|
87
74
|
if (!parentPort) throw new Error("The build worker must run in a worker thread.");
|
|
88
|
-
const { entry } = workerData;
|
|
89
|
-
|
|
75
|
+
const { entry, directory } = workerData;
|
|
76
|
+
await once(parentPort, "message");
|
|
77
|
+
parentPort.postMessage(await buildDeck(entry, directory));
|
|
90
78
|
//#endregion
|
|
91
79
|
export {};
|
|
92
80
|
|
package/dist/worker.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport {
|
|
1
|
+
{"version":3,"file":"worker.mjs","names":[],"sources":["../src/build.ts","../src/worker.ts"],"sourcesContent":["import { build, transform } from 'esbuild';\nimport { readFile } from 'node:fs/promises';\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, directory: string): Promise<BuildResult> {\n const output = join(directory, 'deck.mjs');\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 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-office-kit',\n setup(builder) {\n // Core uses symbol-backed handles; all consumers must share its instance.\n builder.onResolve({ filter: /^@office-kit\\/pptx(?:-dsl)?(?:\\/.*)?$/ }, ({ path }) => ({\n path: resolvePackage(path),\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}\n","import { once } from 'node:events';\nimport { 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, directory } = workerData as { entry: string; directory: string };\nawait once(parentPort, 'message');\nparentPort.postMessage(await buildDeck(entry, directory));\n"],"mappings":";;;;;;;;;;AAcA,MAAM,kBAAkB,SAAiB,cAAc,OAAO,KAAK,QAAQ,IAAI,CAAC;;AAShF,eAAsB,UAAU,OAAe,WAAyC;CACtF,MAAM,SAAS,KAAK,WAAW,UAAU;CACzC,MAAM,SAAS,MAAM,MAAM;EACzB,aAAa,CAAC,QAAQ,KAAK,CAAC;EAC5B,SAAS;EACT,QAAQ;EACR,UAAU;EACV,QAAQ;EACR,QAAQ;EACR,KAAK;EACL,iBAAiB;EACjB,WAAW;EACX,UAAU;EACV,UAAU;EACV,SAAS,CACP;GACE,MAAM;GACN,MAAM,SAAS;IAGb,QAAQ,OAAO,EAAE,QAAQ,kBAAkB,GAAG,OAAO,EAAE,WAAW;KAkBhE,OAAO;MAAE,WAAU,MATO,UAAU,MARf,SAAS,MAAM,MAAM,GAQE;OAC1C,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;OAGN,YAAY;OACZ,WAAW;OACX,KAAK;OACL,QAAQ;OACR,iBAAiB;OACjB,QAAQ,EAAE,mBAAmB,KAAK,UAAU,cAAc,IAAI,CAAC,CAAC,IAAI,EAAE;MACxE,CAAC,EAAA,CAC8B;MAAM,QAAQ;KAAK;IACpD,CAAC;GACH;EACF,GACA;GACE,MAAM;GACN,MAAM,SAAS;IAEb,QAAQ,UAAU,EAAE,QAAQ,wCAAwC,IAAI,EAAE,YAAY;KACpF,MAAM,eAAe,IAAI;KACzB,UAAU;IACZ,EAAE;GACJ;EACF,CACF;CACF,CAAC;CACD,MAAM,SAA6B,MAAM,OAAO,cAAc,MAAM,CAAC,CAAC;CACtE,IAAI,CAAC,OAAO,SAAS,MAAM,IAAI,MAAM,kDAAkD;CACvF,MAAM,eAAe,MAAM,QAAQ,OAAO,OAAO;CACjD,MAAM,cAAc,qBAAqB,YAAY;CACrD,MAAM,SAAS,YAAY,QAAQ,UAAU,MAAM,aAAa,OAAO;CACvE,IAAI,OAAO,QAAQ,MAAM,IAAI,MAAM,yBAAyB,KAAK,UAAU,MAAM,GAAG;CACpF,MAAM,QAAQ,MAAM,iBAAiB,YAAY;CAEjD,MAAM,QAAQ,MAAM,iBAAiB,KAAK;CAC1C,MAAM,OAAO,aAAa,KAAK;CAC/B,OAAO;EACL;EACA,aAAa,OAAO,KAAK,QAAQ,KAAK,SAAS,KAAK;EACpD,QAAQ,UAAU,KAAK,CAAC,CAAC,KAAK,UAAU,iBAAiB,OAAO,KAAK,CAAC;EACtE,cAAc,OAAO,KAAK,OAAO,SAAS,MAAM,CAAC,CAAC,KAAK,SAAS,QAAQ,IAAI,CAAC;EAC7E;CACF;AACF;;;AC1FA,IAAI,CAAC,YAAY,MAAM,IAAI,MAAM,+CAA+C;AAChF,MAAM,EAAE,OAAO,cAAc;AAC7B,MAAM,KAAK,YAAY,SAAS;AAChC,WAAW,YAAY,MAAM,UAAU,OAAO,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@office-kit/pptx-dev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Build and preview PowerPoint TSX presentations locally",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,12 +28,13 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"esbuild": "^0.28.1",
|
|
31
|
-
"@office-kit/pptx": "^0.
|
|
32
|
-
"@office-kit/pptx-preview": "^0.9.
|
|
33
|
-
"@office-kit/pptx-dsl": "^0.
|
|
31
|
+
"@office-kit/pptx": "^0.17.0",
|
|
32
|
+
"@office-kit/pptx-preview": "^0.9.6",
|
|
33
|
+
"@office-kit/pptx-dsl": "^0.4.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^24.13.2",
|
|
37
|
+
"playwright": "^1.63.0",
|
|
37
38
|
"tsdown": "^0.22.2",
|
|
38
39
|
"typescript": "^6.0.3"
|
|
39
40
|
},
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "tsdown",
|
|
45
46
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "node --test test/*.test.mjs"
|
|
47
|
+
"test": "node --test test/*.test.mjs",
|
|
48
|
+
"test:browser": "node --test test/browser/*.test.mjs"
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|