@office-kit/pptx-dev 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -30,7 +30,7 @@ main{min-width:0;min-height:0;display:flex;flex-direction:column}
30
30
  #error[hidden]{display:none}
31
31
  #stage{flex:1;min-height:0;overflow:auto;display:flex;padding:32px;overscroll-behavior:contain}
32
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}
33
+ #slide{user-select:text;-webkit-user-select:text}
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}
@@ -49,13 +49,18 @@ body.presenting{grid-template-rows:minmax(0,1fr);background:#111}
49
49
  <nav class="filmstrip" aria-label="Slides"><h2>Slides</h2><ol id="thumbnails"></ol></nav>
50
50
  <main aria-label="Slide viewer"><pre id="error" role="alert" hidden></pre><div id="stage" tabindex="-1"><div id="empty">Waiting for slides…</div><div id="slide" hidden></div></div></main>
51
51
  </div>
52
- <footer><span id="count" aria-live="polite">No slides</span><span class="hint">Changes appear automatically · View only</span><button id="prev" aria-label="Previous slide" disabled>‹</button><button id="next" aria-label="Next slide" disabled>›</button><label for="zoom">Zoom</label><select id="zoom"><option value="fit">Fit</option><option value="0.5">50%</option><option value="0.75">75%</option><option value="1">100%</option><option value="1.25">125%</option><option value="1.5">150%</option><option value="2">200%</option></select></footer>
52
+ <footer><span id="count" aria-live="polite">No slides</span><span class="hint">Changes appear automatically · Text can be selected and copied</span><button id="prev" aria-label="Previous slide" disabled>‹</button><button id="next" aria-label="Next slide" disabled>›</button><label for="zoom">Zoom</label><select id="zoom"><option value="fit">Fit</option><option value="0.5">50%</option><option value="0.75">75%</option><option value="1">100%</option><option value="1.25">125%</option><option value="1.5">150%</option><option value="2">200%</option></select></footer>
53
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
+ let displayedSvg;
57
57
  const byId=id=>document.getElementById(id);
58
58
  const stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');
59
+ // The slide lives in a shadow root rather than a sandboxed iframe so its text
60
+ // (XHTML inside <foreignObject>) can be selected and copied while the deck's
61
+ // SVG stays out of the viewer's own DOM and CSS. Keyboard events still reach the
62
+ // document, so arrow-key navigation keeps working after clicking into a slide.
63
+ const canvas=slide.attachShadow({mode:'open'});
59
64
  function resize(){
60
65
  if(!state.slides.length)return;
61
66
  const style=getComputedStyle(stage);
@@ -78,21 +83,9 @@ function selectSlide(next,focusThumbnail=false,reveal=true){
78
83
  const svg=state.slides[index];
79
84
  if(svg!==displayedSvg){
80
85
  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();
86
+ canvas.innerHTML=svg?'<style>svg{display:block;width:100%;height:100%}</style>'+svg:'';
94
87
  }
95
- for(const frame of slide.children)frame.title='Slide '+(index+1);
88
+ slide.setAttribute('aria-label','Slide '+(index+1));
96
89
  for(const [position,item] of Array.from(thumbnails.children).entries()){
97
90
  const button=item.firstElementChild,selected=position===index;
98
91
  button.setAttribute('aria-current',String(selected));button.tabIndex=selected?0:-1;
@@ -151,7 +144,7 @@ document.addEventListener('fullscreenchange',()=>{if(!document.fullscreenElement
151
144
  for(const id of ['prev','present-prev'])byId(id).onclick=()=>selectSlide(index-1);
152
145
  for(const id of ['next','present-next'])byId(id).onclick=()=>selectSlide(index+1);
153
146
  byId('zoom').onchange=resize;
154
- stage.onclick=()=>{if(presenting)selectSlide(index+1);};
147
+ stage.onclick=()=>{if(presenting&&!getSelection().toString())selectSlide(index+1);};
155
148
  document.addEventListener('keydown',event=>{
156
149
  if(event.key==='Escape'&&presenting){event.preventDefault();void exitPresentation();return;}
157
150
  if(event.altKey||event.ctrlKey||event.metaKey||event.target.closest('select,input,textarea,[contenteditable]'))return;
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{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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"}
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{user-select:text;-webkit-user-select:text}\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 · Text can be selected and copied</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;\nconst byId=id=>document.getElementById(id);\nconst stage=byId('stage'),slide=byId('slide'),thumbnails=byId('thumbnails');\n// The slide lives in a shadow root rather than a sandboxed iframe so its text\n// (XHTML inside <foreignObject>) can be selected and copied while the deck's\n// SVG stays out of the viewer's own DOM and CSS. Keyboard events still reach the\n// document, so arrow-key navigation keeps working after clicking into a slide.\nconst canvas=slide.attachShadow({mode:'open'});\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 canvas.innerHTML=svg?'<style>svg{display:block;width:100%;height:100%}</style>'+svg:'';\n }\n slide.setAttribute('aria-label','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&&!getSelection().toString())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.d.mts CHANGED
@@ -16,8 +16,25 @@ declare function initProject(directory: string): Promise<string>;
16
16
  //#region src/inspect.d.ts
17
17
  /** Source references usable directly in Slide, Fill and Remove props. */
18
18
  declare function inspectTemplate(file: string): Promise<{
19
- slides: any;
20
- layouts: any;
19
+ slides: {
20
+ index: number;
21
+ part: string;
22
+ title: string | null;
23
+ shapes: {
24
+ id: number;
25
+ name: string;
26
+ kind: "shape" | "picture" | "group" | "graphicFrame" | "connector";
27
+ placeholder: {
28
+ type: string | null;
29
+ idx: number | null;
30
+ };
31
+ }[];
32
+ }[];
33
+ layouts: {
34
+ part: string;
35
+ name: string;
36
+ type: string | null;
37
+ }[];
21
38
  }>;
22
39
  //#endregion
23
40
  //#region src/index.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-kit/pptx-dev",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Build and preview PowerPoint TSX presentations locally",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,9 +28,9 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "esbuild": "^0.28.1",
31
- "@office-kit/pptx": "^0.18.0",
32
- "@office-kit/pptx-dsl": "^0.5.0",
33
- "@office-kit/pptx-preview": "^0.9.6"
31
+ "@office-kit/pptx": "^0.18.2",
32
+ "@office-kit/pptx-preview": "^0.9.7",
33
+ "@office-kit/pptx-dsl": "^0.5.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^24.13.2",