@office-kit/pptx-dev 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,18 +36,95 @@ npm install -D @office-kit/pptx-dev@latest
36
36
  npm run dev
37
37
  ```
38
38
 
39
- Open the local URL printed by the server. Save a slide file, `theme.ts` or `deck.tsx` to rebuild. The viewer
40
- has a vertical thumbnail strip, a large slide canvas and an AI chat panel on the right. Click a thumbnail or use
41
- arrow keys, Page Up/Down, Home/End to navigate. Fit/zoom and Present (Escape to
42
- exit) are viewing controls. Click an object or drag an area for an AI instruction, or double-click text
43
- to save a literal directly. All edits are persisted in TSX.
44
- Keep the server running throughout the edit/review loop. Saving updates only the
45
- changed thumbnails and slide view, preserving zoom, scroll position and presentation
46
- mode. The previous slide stays visible until its replacement is ready. Rapid edits
47
- cancel obsolete evaluations; only the latest successful result is published.
48
- Download PPTX exports the last successful build.
49
- A syntax or runtime error is shown without discarding the last successful preview.
50
- DSL evaluation errors include the TSX element's source file and line number.
39
+ Open the local URL printed by the server. The default editor lets you edit text,
40
+ move and resize objects, and use the slide, insert and formatting tools directly
41
+ in the preview. Switch between English and Japanese from the editor header.
42
+ Committed edits save automatically; **Save** or Ctrl/Cmd+S saves immediately.
43
+ The browser keeps recovery copies of committed, unsaved changes. After reloading,
44
+ choose **Restore changes** or **Discard recovery copy**. Copies are separate for
45
+ each project and editing tab, and stay in this browser at the same preview URL
46
+ (including its port). A restored copy merges with later source changes like any
47
+ unsaved edit while the same server keeps running; after a server restart, a copy
48
+ of an older deck requires conflict resolution. Text still being typed must be
49
+ committed first.
50
+
51
+ Resize handles follow each object's rotation and keep the opposite corner or edge
52
+ fixed. Hold Shift while resizing to preserve the starting aspect ratio. Undo/Redo
53
+ restores the complete gesture; Escape cancels it. Multiple selected objects share
54
+ a rotation handle that turns the selection around its centre while preserving
55
+ spacing and relative angles. Hold Shift to rotate in 15-degree steps. The four
56
+ corners of a multiple selection resize all selected object positions and dimensions
57
+ proportionally, including rotated objects. Font sizes, line widths and other
58
+ appearance attributes keep their existing values.
59
+
60
+ The text input displays explicit character formatting while editing shapes and
61
+ table cells, and resolves inherited run styles for shapes. Text follows the canvas
62
+ zoom while saved font sizes remain unchanged. Ctrl/Cmd+Z and Ctrl/Cmd+Y undo and
63
+ redo pending text edits and continue through committed formatting changes without
64
+ leaving the text input. Ctrl/Cmd+Shift+Z also redoes edits. Escape cancels pending
65
+ text edits. Japanese composition is kept together
66
+ as one undo step. Paragraph alignment, line spacing, spacing before and after,
67
+ indentation and text direction are visible while editing. Bullets and numbered
68
+ lists show their markers without adding characters to the editable text; nested
69
+ numbered lists keep separate counters by level. Tab / Shift+Tab changes the
70
+ selected list paragraphs' levels; Ctrl/Cmd+[ / ] also changes paragraph levels,
71
+ including inside table cells. Tab in tables still moves to the next cell.
72
+ Picture bullets, custom bullet
73
+ font/size overrides, vertical text placement and inherited table character styles
74
+ still use the presentation renderer after committing the edit.
75
+
76
+ Selected text copied or cut within the editor keeps its character formatting
77
+ when pasted into another shape or table cell, including text still being edited.
78
+ Ctrl/Cmd+Shift+V pastes plain text (the browser may request clipboard access).
79
+ Copy also supplies HTML for other applications. Pasting external HTML imports
80
+ inline bold, italic, underline, strike, font, size, color, highlight and baseline
81
+ formatting when its text matches the plain-text clipboard. Unsupported markup
82
+ falls back to plain text; spreadsheet tables keep their cell-aware paste behavior.
83
+ Paragraph styles, hyperlinks, stylesheets and theme inheritance are not transferred.
84
+
85
+ The editor fills the window; the preview adds no header of its own. **Download**
86
+ (Quick Access Toolbar ⋯) exports the edited PPTX, and build progress and errors
87
+ appear in the editor's title bar. **View ▸ Reading View** (or the Reading View
88
+ button in the status bar) switches to the rendered viewer with thumbnails, zoom
89
+ and presentation mode; **Normal** in its status bar, or Esc, returns to the
90
+ editor. The Slide Show tab starts the presentation and presenter view. Presentation playback animates
91
+ fade, push, wipe, cover, uncover, and zoom using the saved speed and direction.
92
+ Other effects currently switch immediately. Reduced-motion preferences disable
93
+ these animations; automatic slide timing starts after each animation completes.
94
+
95
+ The viewer has a vertical thumbnail strip, a large slide canvas and a status bar
96
+ with the slide count, build status, Present, Presenter view, Download PPTX,
97
+ Agents, Normal and zoom. Click a thumbnail or use arrow keys, Page Up/Down,
98
+ Home/End to navigate; Fit/zoom and Present (Escape to exit) are viewing controls. Click an
99
+ object or drag an area for an AI instruction.
100
+
101
+ Canvas edits are saved beside the entry in `.office-kit/<entry-name>.editor.zip`;
102
+ for example, `.office-kit/deck.tsx.editor.zip`. Keep this file with your project:
103
+ it contains the edited presentation and the source build it was edited from.
104
+ Canvas edits do not rewrite TSX. CLI `build`, `buildDeck` and `exportDeck` use
105
+ the saved edited presentation, including after restarting the server.
106
+
107
+ Saving a slide file, `theme.ts` or `deck.tsx` rebuilds the source. Changes from
108
+ TSX merge with canvas edits, saved or not yet saved: shapes, slide settings,
109
+ media and links changed on only one side are combined, and the editor reloads
110
+ the merged presentation, which starts a new Undo history. The editor asks you
111
+ to choose **Keep my edits** or **Use source** only when the same item was
112
+ changed in both places, such as one shape edited on the canvas and in TSX, a
113
+ shape deleted on one side and changed on the other, or slides added, removed or
114
+ reordered on both sides. The message names each collision, for example
115
+ "Slide 2: Title 1 was changed both here and in the source." Keeping edits keeps
116
+ the entire edited deck without the TSX changes; using source discards the saved
117
+ canvas version. You can download the source version before choosing. CLI export
118
+ refuses an unresolved conflict so that it cannot silently export the wrong
119
+ version. Editor files from versions before merging was available cannot merge
120
+ until the conflict is resolved once.
121
+
122
+ Keep the server running throughout the edit/review loop. In Preview mode, saving
123
+ updates changed thumbnails and slides while preserving zoom, scroll and
124
+ presentation mode. Rapid source edits cancel obsolete evaluations; only the latest successful result
125
+ is published. Syntax and
126
+ runtime errors remain visible alongside the last successful preview; DSL errors
127
+ include the TSX element's source file and line number.
51
128
 
52
129
  ```sh
53
130
  npm run check
@@ -72,7 +149,10 @@ slides. Update time depends on the deck size and any code it runs.
72
149
 
73
150
  ## Agents in the preview
74
151
 
75
- The right panel defaults to **Claude Code**. Click **Start** to open the locally
152
+ **Agents** at the right end of the editor's tab row, beside Comments and Share,
153
+ opens the Agents task pane docked on the right (closed by default, like
154
+ PowerPoint's panes; × closes it). The pane follows the editor's light or dark
155
+ appearance, including the terminal colors. Each agent defaults to **Claude Code**. Click **Start** to open the locally
76
156
  installed `claude` CLI in an interactive terminal. Install and sign in to Claude
77
157
  Code first. Use a current version with HTTP `UserPromptSubmit` hook support.
78
158
  The session runs in the deck entry's directory and uses your normal account,
@@ -131,22 +211,22 @@ each request. The dev server retains up to 100 messages until it exits.
131
211
  sends a message. Model/settings menus in the embedded terminal are currently
132
212
  specific to Claude Code.
133
213
 
134
- Use the **Split right** or **Split down** icons in a pane's title bar to run up to
214
+ Use the **Split right** or **Split down** icons in an agent's toolbar to run up to
135
215
  four agents side by side or stacked. Each pane independently selects Claude Code
136
216
  or Codex, retains its own conversation and receives the currently selected slide.
137
217
  Drag the divider to adjust the split, or focus it and use the arrow keys. Layout
138
218
  and Claude terminal sessions survive a browser reload. Splitting preserves running
139
- sessions and unsent drafts. **Close** ends only that pane's agent; closing the last
219
+ sessions and unsent drafts. **Close agent** ends only that agent; closing the last
140
220
  pane opens a fresh one. All agents edit the same project directory, so assign
141
221
  separate slides or files when working simultaneously.
142
222
 
143
- Drag the left edge of the chat panel to adjust its width. The preview remembers the
223
+ Drag the left edge of the Agents pane to adjust its width. The preview remembers the
144
224
  width in this browser; double-click the edge to reset it. You can also focus the
145
225
  edge and use the left/right arrow keys (Shift for larger steps), or Home/End for
146
226
  the minimum/maximum width. Narrow screens keep the stacked layout.
147
227
 
148
- Use **Agents** in the header to hide/show the panel. On narrow screens the panel
149
- moves below the preview. Presentation mode hides it without ending the session.
228
+ The pane stays open across reloads of the same tab. On narrow screens it floats
229
+ over the editor's right edge, and in the viewer it moves below the slide. Presentation mode hides it without ending the session.
150
230
  Both providers save TSX directly, which triggers the usual automatic rebuild.
151
231
  The server binds to loopback and requires same-origin JSON for terminal control;
152
232
  the local context hook uses a separate per-session token.
@@ -0,0 +1 @@
1
+ var fe=["appear","disappear"],me=["fadeIn","appear","flyIn","zoomIn"],pe=["fadeOut","disappear","flyOut","zoomOut"],ye=["spin"],se={appear:"none",disappear:"none",fadeIn:"fade",fadeOut:"fade",flyIn:"fly",flyOut:"fly",zoomIn:"zoom",zoomOut:"zoom",spin:"spin"},ge=360,te=t=>t instanceof HTMLElement||t instanceof SVGElement?t:null,he=t=>t.effect===null?null:me.includes(t.effect)?"entrance":pe.includes(t.effect)?"exit":ye.includes(t.effect)?"emphasis":null,be=t=>t.durationMs!==null&&Number.isFinite(t.durationMs)&&t.durationMs>=0?t.durationMs:t.effect!==null&&fe.includes(t.effect)?0:null,Se=t=>{let m=[],S=[],r=(l,E)=>{S.push({step:l,reason:E})},p=null,c=0,i=0,a=!1;for(let l of t??[]){if(l.sequence!=="mainSeq"){r(l,"notInMainSeq");continue}let E=!a;if(a=!0,l.start==="unknown"){c=null,i=null,r(l,"notModelled");continue}l.start==="click"||E?(p={items:[],auto:E&&l.start!=="click"},m.push(p),c=0,i=0):l.start==="afterPrevious"&&(c=i,i=c);let h=l.playable?he(l):null,I=h===null?null:be(l),M=l.delayMs;if(h===null||M===null||c===null){r(l,h===null||M===null?"notModelled":"unknownTiming"),i=null;continue}let T=c+M;p?.items.push({step:l,kind:h,begin:T,duration:I}),i=I===null||i===null?null:Math.max(i,T+I)}return{stops:m,unsupported:S}},le=t=>`[data-pptx-shape-id="${t}"]`,oe=(t,m,S)=>{let r=m.target;if(r.kind!=="shape"&&r.kind!=="paragraphs")return[];if(!Number.isInteger(r.shapeId))return[];let p=String(r.shapeId);if(S.has(p))return[];let c=t.querySelectorAll(le(p)),i=c.length===1?te(c[0]):null;if(i===null)return[];if(r.kind!=="paragraphs")return[i];let a=[];for(let l of i.querySelectorAll("[data-pptx-paragraph]")){if(l.closest("[data-pptx-shape-id]")!==i)continue;let E=Number(l.getAttribute("data-pptx-paragraph"));if(E<r.firstParagraph||E>r.lastParagraph)continue;let h=te(l);h!==null&&a.push(h)}return a},re=()=>typeof performance=="object"?performance.now():Date.now(),Ee=(t,m)=>{let S=t.closest("svg");return S!==null?S.getBoundingClientRect():m instanceof Element?m.getBoundingClientRect():null},ve=t=>{if(t instanceof SVGGraphicsElement)return t.getScreenCTM();let m=t.closest("foreignObject");return m instanceof SVGGraphicsElement?m.getScreenCTM():null},Ae=(t,m,S)=>{let r=Ee(t,S);if(r===null||r.width<=0||r.height<=0)return null;let p=t.getBoundingClientRect(),c=m==="right"?r.right-p.left:m==="left"?r.left-p.right:0,i=m==="bottom"?r.bottom-p.top:m==="top"?r.top-p.bottom:0,a=ve(t);if(a===null)return{dx:c,dy:i};let l=a.a*a.d-a.b*a.c;return l===0?null:{dx:(a.d*c-a.c*i)/l,dy:(a.a*i-a.b*c)/l}},Ie=t=>t.step.effect!==null&&se[t.step.effect]==="fly",xe=(t,m,S)=>{let r=t.step.effect,p=t.kind==="entrance";switch(r===null?"none":se[r]??"none"){case"none":return null;case"fade":return{frames:p?[{opacity:0},{opacity:1}]:[{opacity:1},{opacity:0}],aboutOwnCentre:!1};case"zoom":{let c={scale:"0"},i={scale:"1"};return{frames:p?[c,i]:[i,c],aboutOwnCentre:!0}}case"spin":return{frames:[{rotate:"0deg"},{rotate:`${ge}deg`}],aboutOwnCentre:!0};case"fly":{let c=t.step.direction;if(c===null)return null;let i=Ae(m,c,S);if(i===null)return null;let a={translate:`${i.dx}px ${i.dy}px`},l={translate:"0px 0px"};return{frames:p?[a,l]:[l,a],aboutOwnCentre:!1}}}},Me=t=>{let m=()=>typeof t.root=="function"?t.root():t.root,S=Se(t.steps),r=S.stops,p=[...S.unsupported],c=()=>t.reducedMotion?.()===!0,i=t.onChange??(()=>{}),a=[],l=[],E=new Set,h=new Map,I=new Map,M=new Map,T=new Set,g=0,P=!1,O=null,k=0,R=new Set;for(let e of p)for(let n of e.step.targetShapeIds)R.add(String(n));let z=new Map;for(let e of r)for(let n of e.items){let s=String(n.step.target.shapeId);z.has(s)||z.set(s,m().querySelectorAll(le(s)).length)}for(let e of r)for(let n of e.items)(z.get(String(n.step.target.shapeId))??0)>1&&p.push({step:n.step,reason:"ambiguousTarget"});for(let[e,n]of z)n>1&&R.add(e);let ie=e=>{let n=new Set;for(let s of r){let f=new Map;for(let o of s.items){let u=o.duration===null?Number.POSITIVE_INFINITY:o.begin+o.duration;for(let y of oe(e,o.step,R)){let d={item:o,begin:o.begin,end:u},b=f.get(y);b===void 0?f.set(y,[d]):b.push(d)}}for(let o of f.values()){if(o.length<2)continue;o.sort((d,b)=>d.begin-b.begin);let u=Number.NEGATIVE_INFINITY;for(let d of o)d.begin<u&&n.add(d.item),u=Math.max(u,d.end);let y=Number.POSITIVE_INFINITY;for(let d=o.length-1;d>=0;d-=1){let b=o[d];b!==void 0&&(b.end>y&&n.add(b.item),y=Math.min(y,b.begin))}}}for(let s of r){let f=0;for(let o of s.items){if(o.step.valueAfterEnd==="unstated"&&n.has(o)){p.push({step:o.step,reason:"notModelled"});for(let u of o.step.targetShapeIds)R.add(String(u));continue}s.items[f]=o,f+=1}s.items.length=f}},C=()=>{for(let e of l)clearTimeout(e);l.length=0;for(let e of a)e.cancel();a.length=0;for(let e of I.values())for(let n of e)n.cancel();I.clear(),h.clear(),M.clear(),T.clear()},$=(e,n)=>{let s=e.indexOf(n);s>=0&&e.splice(s,1)},Y=()=>{P||(l.length===0&&a.length===0&&(O=null),i(x))},ae=e=>{E.add(e),e.style.transformBox="fill-box",e.style.transformOrigin="50% 50%"},U=e=>{e.style.transformBox="",e.style.transformOrigin=""},ue=e=>{E.add(e),e.style.visibility="",e.style.opacity="",U(e)},de=e=>{E.add(e),e.style.visibility="hidden",e.style.opacity="",U(e)},H=null,V=!0,w=new Map,B=new Map,K=0,L=e=>e.stopIndex<K||T.has(e.item),X=()=>{for(let e of w.values()){e.top=null;for(let s of e.gates)L(s)&&(e.top=s);let n=e.gates[0];e.firstPlayed=n!==void 0&&L(n)}},G=()=>{let e=m(),n=w.keys().next();if(!(e===H&&n.done!==!0&&n.value.isConnected&&V)){H=e,V=!0,ie(e),w=new Map,B=new Map,r.forEach((s,f)=>{for(let o of s.items){let u=[];for(let y of oe(e,o.step,R)){let d=w.get(y)??{list:[],gates:[],top:null,firstPlayed:!1};w.set(y,d);let b={item:o,stopIndex:f,order:0};d.list.push(b),o.kind!=="emphasis"&&d.gates.push(b);let A=xe(o,y,e);A===null&&Ie(o)&&(V=!1),u.push({el:y,plan:d,entry:b,movement:A})}B.set(o,u)}});for(let s of w.values()){let f=(o,u)=>o.stopIndex-u.stopIndex||o.item.begin-u.item.begin;s.list.sort(f),s.gates.sort(f),s.list.forEach((o,u)=>{o.order=u})}X()}},ce=e=>{let n=e.gates[0];return n===void 0?!0:n.item.kind==="entrance"&&!e.firstPlayed?!1:e.top===null||e.top.item.kind!=="exit"},J=(e,n)=>{h.has(e)||(ce(n)?ue(e):de(e))},F=e=>{T.clear(),K=e,G(),X();for(let[n,s]of w)J(n,s)},j=e=>{let{plan:n,entry:s}=e;s.item.kind!=="emphasis"&&(s===n.gates[0]&&(n.firstPlayed=!0),(n.top===null||s.order>n.top.order)&&(n.top=s)),J(e.el,n)},D=e=>{T.add(e),G();for(let n of B.get(e)??[])j(n)},Q=(e,n)=>{G();let s=B.get(e)??[];if(s.length===0){D(e);return}let f=s.filter(A=>A.movement!==null),o=e.duration,u=Math.max(0,n),y=o===null||o===0||u>=o,d=e.step.valueAfterEnd!=="removed";if(c()||f.length===0||y&&(!d||!f.some(A=>h.has(A.el)))){D(e);return}for(let A of s)A.movement===null&&j(A);let b=y?0:o;M.set(e,f.length);for(let A of f){let v=A.el,ee=A.movement;E.add(v),e.kind!=="emphasis"&&(v.style.visibility=""),ee.aboutOwnCentre&&ae(v),h.set(v,(h.get(v)??0)+1);let N=v.animate(ee.frames,{duration:b,fill:"both"});N.currentTime=y?b:u,a.push(N),N.finished.then(()=>{if(P)return;$(a,N);let _=(h.get(v)??1)-1;if(_<=0||!d){if(_<=0){h.delete(v);for(let q of I.get(v)??[])q.cancel();I.delete(v)}else h.set(v,_);N.cancel()}else{h.set(v,_);let q=I.get(v);q===void 0?I.set(v,[N]):q.push(N)}j(A);let ne=(M.get(e)??1)-1;M.set(e,ne),ne<=0&&D(e),Y()},()=>{})}},W=(e,n)=>{let s=r[e];if(s===void 0)return;let f=Math.max(0,n);O=re()-f;let o=[...s.items].sort((u,y)=>u.begin-y.begin);for(let u of o){let y=u.begin-f;if(c()||y<=0){Q(u,f-u.begin);continue}let d=setTimeout(()=>{$(l,d),Q(u,0),Y()},y);l.push(d)}},Z=e=>Math.max(0,Math.min(Number.isFinite(e)?Math.round(e):0,r.length)),x={get cursor(){return g},get stopCount(){return r.length},get pending(){return g<r.length},get running(){return l.length>0||a.length>0},get progress(){return{cursor:g,stops:r.length,elapsed:O===null?null:Math.round(re()-O),generation:k}},get unsupported(){return[...p]},reset(){k+=1,C(),O=null,g=0,F(0),i(x),r[0]?.auto===!0&&x.advance()},advance(){if(P||g>=r.length)return!1;k+=1,C(),F(g);let e=g;return g+=1,W(e,0),i(x),!0},back(){return P||g<=0?!1:(k+=1,C(),O=null,g-=1,F(g),i(x),!0)},jumpTo(e){P||(k+=1,C(),O=null,g=Z(e),F(g),i(x))},resume(e,n){P||(k+=1,C(),O=null,g=Z(e),g>0&&n!==null&&Number.isFinite(n)?(F(g-1),W(g-1,n)):F(g),i(x))},finish(){x.jumpTo(r.length)},dispose(){if(!P){P=!0,C();for(let e of E)e.style.visibility="",e.style.opacity="",U(e);E.clear()}}};return G(),x};export{Se as buildAnimationStops,Me as createAnimationPlayer};
@@ -0,0 +1,131 @@
1
+ import { join } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { createHash } from "node:crypto";
4
+ import { unzipSync } from "fflate";
5
+ import { compile, getShapeJsxSources } from "@office-kit/pptx-dsl";
6
+ import { findShapesWithMedia, getCustomShows, getGroupChildren, getShapeBounds, getShapeId, getShapeMedia, getShapeMediaPlayback, getSlideAnimations, getSlideNotes, getSlidePartName, getSlideShapes, getSlideShowProperties, getSlideSize, getSlideText, getSlideTransition, getSlides, isSlideHidden, loadPresentation, savePresentation, validatePresentation } from "@office-kit/pptx";
7
+ import { renderSlideToSvg } from "@office-kit/pptx-preview";
8
+ //#region src/preview-cache.ts
9
+ /** Reuse SVG only when both slide XML and every shared rendering resource match. */
10
+ function renderPreview(presentation, bytes, previous) {
11
+ const slides = getSlides(presentation);
12
+ const names = slides.map((slide) => getSlidePartName(slide).replace(/^\//, ""));
13
+ const slideParts = new Set(names);
14
+ const parts = unzipSync(bytes);
15
+ const hashes = new Map(Object.keys(parts).sort().map((name) => [name, createHash("sha256").update(parts[name]).digest("hex")]));
16
+ const shared = createHash("sha256").update(JSON.stringify([...hashes].filter(([name]) => !slideParts.has(name) && !name.startsWith("docProps/")))).digest("hex");
17
+ const cache = {
18
+ shared,
19
+ slides: {}
20
+ };
21
+ const rendered = slides.map((slide, index) => {
22
+ const name = names[index];
23
+ const hash = hashes.get(name);
24
+ const cached = previous?.shared === shared ? previous.slides[name] : void 0;
25
+ const result = cached?.hash === hash ? cached : {
26
+ hash,
27
+ svg: renderSlideToSvg(presentation, slide),
28
+ text: getSlideText(slide)
29
+ };
30
+ cache.slides[name] = result;
31
+ return result;
32
+ });
33
+ return {
34
+ slides: rendered.map((slide) => slide.svg),
35
+ slideTexts: rendered.map((slide) => slide.text),
36
+ cache
37
+ };
38
+ }
39
+ //#endregion
40
+ //#region src/media-manifest.ts
41
+ const dataUrl = (contentType, bytes) => `data:${contentType};base64,${Buffer.from(bytes).toString("base64")}`;
42
+ /**
43
+ * Collects media shapes without changing the rendered SVG. Embedded clips are
44
+ * made self-contained data URLs so the dev server does not need to expose raw
45
+ * OPC package parts; the presentation page can replace the matching preview
46
+ * shape with an HTML media element using the stable shape id.
47
+ */
48
+ const getPreviewMedia = (presentation) => getSlides(presentation).flatMap((slide, slideIndex) => findShapesWithMedia(slide).flatMap((shape) => {
49
+ const media = getShapeMedia(shape);
50
+ const bounds = getShapeBounds(shape);
51
+ if (media === null || bounds === null) return [];
52
+ const embedded = media.kind !== "online";
53
+ return [{
54
+ slideIndex,
55
+ shapeId: getShapeId(shape),
56
+ kind: media.kind,
57
+ src: embedded ? dataUrl(media.contentType, media.bytes) : media.url,
58
+ ...embedded ? { contentType: media.contentType } : {},
59
+ bounds,
60
+ playback: embedded ? getShapeMediaPlayback(shape) : null
61
+ }];
62
+ }));
63
+ //#endregion
64
+ //#region src/build.ts
65
+ /** Evaluates trusted local TSX. This is code execution, not a sandbox. */
66
+ async function buildDeck(directory, dependencies, previous) {
67
+ const module = await import(pathToFileURL(join(directory, "deck.mjs")).href);
68
+ if (!module.default) throw new Error("The TSX file must default-export a Presentation.");
69
+ const presentation = await compile(module.default);
70
+ const errors = validatePresentation(presentation).filter((issue) => issue.severity === "error");
71
+ if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);
72
+ const shapeSources = jsxSourcesBySlide(presentation);
73
+ const rendered = await renderDeck(await savePresentation(presentation), dependencies, previous);
74
+ return {
75
+ ...rendered,
76
+ result: {
77
+ ...rendered.result,
78
+ shapeSources
79
+ }
80
+ };
81
+ }
82
+ function jsxSourcesBySlide(presentation) {
83
+ const result = {};
84
+ getSlides(presentation).forEach((slide, index) => {
85
+ const byId = {};
86
+ const visit = (shape) => {
87
+ const sources = getShapeJsxSources(shape);
88
+ if (sources) byId[getShapeId(shape)] = sources;
89
+ for (const child of getGroupChildren(shape)) visit(child);
90
+ };
91
+ for (const shape of getSlideShapes(slide)) visit(shape);
92
+ result[index] = byId;
93
+ });
94
+ return result;
95
+ }
96
+ async function renderDeck(bytes, dependencies, previous) {
97
+ const saved = await loadPresentation(bytes);
98
+ const diagnostics = validatePresentation(saved);
99
+ const errors = diagnostics.filter((issue) => issue.severity === "error");
100
+ if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);
101
+ const size = getSlideSize(saved);
102
+ const { slides, slideTexts, cache } = renderPreview(saved, bytes, previous);
103
+ const deckSlides = getSlides(saved);
104
+ const slideIndices = new Map(deckSlides.map((slide, index) => [slide, index]));
105
+ return {
106
+ cache,
107
+ result: {
108
+ bytes,
109
+ aspectRatio: size ? size.width / size.height : 16 / 9,
110
+ slides,
111
+ dependencies,
112
+ slideTexts,
113
+ notes: getSlides(saved).map(getSlideNotes),
114
+ hiddenSlides: getSlides(saved).map(isSlideHidden),
115
+ transitions: getSlides(saved).map(getSlideTransition),
116
+ animations: getSlides(saved).map(getSlideAnimations),
117
+ showProperties: getSlideShowProperties(saved),
118
+ customShows: getCustomShows(saved).map((show) => ({
119
+ id: show.id,
120
+ name: show.name,
121
+ slideIndices: show.slides.map((slide) => slideIndices.get(slide))
122
+ })),
123
+ media: getPreviewMedia(saved),
124
+ diagnostics
125
+ }
126
+ };
127
+ }
128
+ //#endregion
129
+ export { renderDeck as n, buildDeck as t };
130
+
131
+ //# sourceMappingURL=build-cpmt-r9Q.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-cpmt-r9Q.mjs","names":[],"sources":["../src/preview-cache.ts","../src/media-manifest.ts","../src/build.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { unzipSync } from 'fflate';\nimport { getSlidePartName, getSlides, getSlideText, type PresentationData } from '@office-kit/pptx';\nimport { renderSlideToSvg } from '@office-kit/pptx-preview';\n\nexport interface PreviewCache {\n shared: string;\n slides: Record<string, { hash: string; svg: string; text: string }>;\n}\n\n/** Reuse SVG only when both slide XML and every shared rendering resource match. */\nexport function renderPreview(\n presentation: PresentationData,\n bytes: Uint8Array,\n previous?: PreviewCache,\n) {\n const slides = getSlides(presentation);\n const names = slides.map((slide) => getSlidePartName(slide).replace(/^\\//, ''));\n const slideParts = new Set(names);\n const parts = unzipSync(bytes);\n const hashes = new Map(\n Object.keys(parts)\n .sort()\n .map((name) => [name, createHash('sha256').update(parts[name]!).digest('hex')]),\n );\n // Relationships, themes, layouts, charts and media all invalidate the cache.\n // Document metadata changes on each save but cannot affect slide rendering.\n const shared = createHash('sha256')\n .update(\n JSON.stringify(\n [...hashes].filter(([name]) => !slideParts.has(name) && !name.startsWith('docProps/')),\n ),\n )\n .digest('hex');\n const cache: PreviewCache = { shared, slides: {} };\n const rendered = slides.map((slide, index) => {\n const name = names[index]!;\n const hash = hashes.get(name)!;\n const cached = previous?.shared === shared ? previous.slides[name] : undefined;\n const result =\n cached?.hash === hash\n ? cached\n : { hash, svg: renderSlideToSvg(presentation, slide), text: getSlideText(slide) };\n cache.slides[name] = result;\n return result;\n });\n return {\n slides: rendered.map((slide) => slide.svg),\n slideTexts: rendered.map((slide) => slide.text),\n cache,\n };\n}\n","import {\n findShapesWithMedia,\n getShapeBounds,\n getShapeId,\n getShapeMedia,\n getShapeMediaPlayback,\n type MediaPlayback,\n type ShapeBounds,\n type ShapeMedia,\n} from '@office-kit/pptx';\nimport { getSlides } from '@office-kit/pptx';\n\n/** Media information exposed to the dev preview for a single slide shape. */\nexport interface PreviewMedia {\n readonly slideIndex: number;\n readonly shapeId: number;\n readonly kind: ShapeMedia['kind'];\n /** A data URL for embedded media, or the external URL for online media. */\n readonly src: string;\n /** Present for embedded media and suitable for an HTML media element. */\n readonly contentType?: string;\n /** Original shape geometry in EMUs. */\n readonly bounds: ShapeBounds;\n readonly playback: MediaPlayback | null;\n}\n\nconst dataUrl = (contentType: string, bytes: Uint8Array): string =>\n `data:${contentType};base64,${Buffer.from(bytes).toString('base64')}`;\n\n/**\n * Collects media shapes without changing the rendered SVG. Embedded clips are\n * made self-contained data URLs so the dev server does not need to expose raw\n * OPC package parts; the presentation page can replace the matching preview\n * shape with an HTML media element using the stable shape id.\n */\nexport const getPreviewMedia = (presentation: Parameters<typeof getSlides>[0]): PreviewMedia[] =>\n getSlides(presentation).flatMap((slide, slideIndex) =>\n findShapesWithMedia(slide).flatMap((shape) => {\n const media = getShapeMedia(shape);\n const bounds = getShapeBounds(shape);\n if (media === null || bounds === null) return [];\n const embedded = media.kind !== 'online';\n return [\n {\n slideIndex,\n shapeId: getShapeId(shape),\n kind: media.kind,\n src: embedded ? dataUrl(media.contentType, media.bytes) : media.url,\n ...(embedded ? { contentType: media.contentType } : {}),\n bounds,\n playback: embedded ? getShapeMediaPlayback(shape) : null,\n },\n ];\n }),\n );\n","import { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { compile, getShapeJsxSources, type JsxSource, type Node } from '@office-kit/pptx-dsl';\nimport {\n getGroupChildren,\n getShapeId,\n getSlideShapes,\n type PresentationData,\n type SlideShapeData,\n getSlideAnimations,\n getCustomShows,\n getSlideShowProperties,\n getSlideSize,\n getSlides,\n getSlideNotes,\n isSlideHidden,\n getSlideTransition,\n loadPresentation,\n savePresentation,\n validatePresentation,\n} from '@office-kit/pptx';\nimport { renderPreview, type PreviewCache } from './preview-cache.ts';\nimport { getPreviewMedia, type PreviewMedia } from './media-manifest.ts';\n\nexport interface BuildResult {\n bytes: Uint8Array;\n slides: string[];\n slideTexts: string[];\n notes: (string | null)[];\n hiddenSlides: boolean[];\n transitions: ReturnType<typeof getSlideTransition>[];\n /** What each slide animates, in click order — the preview's player reads this. */\n animations: ReturnType<typeof getSlideAnimations>[];\n showProperties: ReturnType<typeof getSlideShowProperties>;\n customShows: { id: number; name: string; slideIndices: number[] }[];\n /** Media clips keyed by slide and shape id for HTML playback in preview mode. */\n media: PreviewMedia[];\n aspectRatio: number;\n dependencies: string[];\n /**\n * Slide index → shape id → the JSX elements being evaluated when the shape\n * was made, outermost first. Only decks built from TSX have it; a saved\n * editor deck has no source to point at.\n */\n shapeSources?: Record<number, Record<number, readonly JsxSource[]>>;\n diagnostics: ReturnType<typeof validatePresentation>;\n}\n/** Evaluates trusted local TSX. This is code execution, not a sandbox. */\nexport async function buildDeck(\n directory: string,\n dependencies: string[],\n previous?: PreviewCache,\n): Promise<{ result: BuildResult; cache: PreviewCache }> {\n const output = join(directory, 'deck.mjs');\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 shapeSources = jsxSourcesBySlide(presentation);\n const bytes = await savePresentation(presentation);\n // Preview serialized output too, so persistence defects are visible during authoring.\n const rendered = await renderDeck(bytes, dependencies, previous);\n return { ...rendered, result: { ...rendered.result, shapeSources } };\n}\n\n// Shape ids survive saving, so sources read before `savePresentation` still\n// name the shapes of the saved deck the editor loads.\nfunction jsxSourcesBySlide(\n presentation: PresentationData,\n): Record<number, Record<number, readonly JsxSource[]>> {\n const result: Record<number, Record<number, readonly JsxSource[]>> = {};\n getSlides(presentation).forEach((slide, index) => {\n const byId: Record<number, readonly JsxSource[]> = {};\n const visit = (shape: SlideShapeData) => {\n const sources = getShapeJsxSources(shape);\n if (sources) byId[getShapeId(shape)] = sources;\n for (const child of getGroupChildren(shape)) visit(child);\n };\n for (const shape of getSlideShapes(slide)) visit(shape);\n result[index] = byId;\n });\n return result;\n}\n\nexport async function renderDeck(\n bytes: Uint8Array,\n dependencies: string[],\n previous?: PreviewCache,\n): Promise<{ result: BuildResult; cache: PreviewCache }> {\n const saved = await loadPresentation(bytes);\n const diagnostics = validatePresentation(saved);\n const errors = diagnostics.filter((issue) => issue.severity === 'error');\n if (errors.length) throw new Error(`Invalid presentation: ${JSON.stringify(errors)}`);\n const size = getSlideSize(saved);\n const { slides, slideTexts, cache } = renderPreview(saved, bytes, previous);\n const deckSlides = getSlides(saved);\n const slideIndices = new Map(deckSlides.map((slide, index) => [slide, index]));\n return {\n cache,\n result: {\n bytes,\n aspectRatio: size ? size.width / size.height : 16 / 9,\n slides,\n dependencies,\n slideTexts,\n notes: getSlides(saved).map(getSlideNotes),\n hiddenSlides: getSlides(saved).map(isSlideHidden),\n transitions: getSlides(saved).map(getSlideTransition),\n animations: getSlides(saved).map(getSlideAnimations),\n showProperties: getSlideShowProperties(saved),\n customShows: getCustomShows(saved).map((show) => ({\n id: show.id,\n name: show.name,\n slideIndices: show.slides.map((slide) => slideIndices.get(slide)!),\n })),\n media: getPreviewMedia(saved),\n diagnostics,\n },\n };\n}\n"],"mappings":"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"}