pptx-generator

pptx-generator is a skill for Claude Code, Codex from guyoung/boxagnts. It costs 45 tokens per session (21,925 once invoked), scanned A, original, MIT.

A JavaScript tool that turns Markdown into Microsoft PowerPoint (.pptx) presentations, including layouts, links, and local images.

In plain words
What is it for?
Use it to generate presentation files from Markdown and embed images stored on the local computer.
Why use it?
It removes the need to construct PowerPoint’s internal file structure and slide layout manually.

Skill for Claude CodeCodex

Install

Getting it into your agent

One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.

agentmods
npx agentmods add skills/guyoung/boxagnts/pptx-generator
Any agent
npx skills add guyoung/boxagnts --skill pptx-generator
Clone the repo
git clone --depth 1 https://github.com/guyoung/boxagnts

Made for: Claude Code, Codex.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for pptx-generator

README.md
[![agentmods](https://agentmods.dev/badge/skills/guyoung/boxagnts/pptx-generator.svg)](https://agentmods.dev/skills/guyoung/boxagnts/pptx-generator)
Your own site
<a href="https://agentmods.dev/skills/guyoung/boxagnts/pptx-generator"><img src="https://agentmods.dev/badge/skills/guyoung/boxagnts/pptx-generator.svg" alt="Measured on agentmods" height="20"></a>
Per session 45 Skills are progressive disclosure: only the name and description are preloaded; the body loads when the skill is used.
When invoked 21,925 The whole file, excluding the scripts and references it only reads on demand.
Security scan A 0 findings. Scan, not verified.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5 $0.00045 $0.21925
Opus 5 $0.00023 $0.10963
Sonnet 5 $0.00009 $0.04385
Haiku 4.5 $0.00005 $0.02193

Measured 5d ago against content hash d11b9df5ef7e, method: parsed. Prices are Anthropic first-party input rates as of 2026-08-30, from the pricing page.

Security

Grade A, and why

pptx-generator scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 5d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Nothing flagged

None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.

app/extensions/skills/pptx-generator/SKILL.md · 1,510 lines

How it starts

The opening of the file, as written. The whole thing — 1,510 lines — stays where its author put it; the contents beside it link to each section on GitHub.

CODE SKILL — VERBATIM REQUIRED This skill targets a formal specification (OOXML). Adaptation is prohibited per C2-A Rule 3.

CRITICAL: Execution Constraints

The code below is the SOLE IMPLEMENTATION — it MUST NOT be replaced, simplified, or reimplemented.

Prohibited deviations:

  • Do NOT write your own crc32(), ZipBuilder, parseMarkdownAST(), buildSlideIR(), layoutSlide(), renderSlideXML()
  • Do NOT skip any OOXML file from the 13+ core file baseline
  • Do NOT use compression (method 8 / DEFLATE) — STORE mode only (method 0)
  • Do NOT skip scanAndAssignLinks() — hyperlinks MUST be registered as relationships
  • Do NOT use remote URLs for images — images MUST be local file paths

Correct Invocation Pattern

  1. Copy the complete script from ## Runnable Entry Point below
  2. Replace ONLY {{markdown}}, {{output_path}}, {{show_slide_numbers}} with actual values
  3. Execute as a single jsexec call with NO modifications

Argument Details

Arg Type Required Default Description
markdown string yes Markdown input for slide generation
output_path string yes Output .pptx file path
show_slide_numbers boolean no false Show N / M page numbers

Runnable Entry Point (COMPLETE IIFE, DO NOT MODIFY)

// ===== Runnable Entry Point (IIFE) - DO NOT MODIFY =====
(async function() {
  const fs = require("fs");
  const pathModule = require("path");

  // -- XML Escape --
  function esc(s) {
    return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
  }

  // -- ID Generators --
  let shapeId = 1;
  let relId = 2; // reserve rId1 for slideLayout in slideRels
  let imageIndex = 1;
  function nextShapeId() { return shapeId++; }
  function nextRelId() { return 'rId' + relId++; }
  function nextImageName(ext = 'png') { return `image${imageIndex++}.${ext}`; }

  // -- CRC32 (ZIP REQUIRED) --
  const CRCT = new Uint32Array(256);
  for (let i = 0; i < 256; i++) {
    let c = i;
    for (let j = 0; j < 8; j++) {
      c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
    }
    CRCT[i] = c;
  }
  function crc32(buf) {
    let c = 0xFFFFFFFF;
    for (let i = 0; i < buf.length; i++) {
      c = CRCT[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
    }
    return (c ^ 0xFFFFFFFF) >>> 0;
  }

  // -- ZIP Builder (STORE mode -- no compression, delayed-concat optimization) --
  class ZipBuilder {
    constructor() {
      this.localChunks = [];
      this.centralChunks = [];
      this.offset = 0;
      this.count = 0;
    }
    add(path, content) {
      const buf = typeof content === 'string' ? Buffer.from(content, 'utf-8') : content;
      const crc = crc32(buf);
      const nameBuf = Buffer.from(path, 'utf-8');
      const localHeader = Buffer.alloc(30);
      localHeader.writeUInt32LE(0x04034b50, 0);
      localHeader.writeUInt16LE(20, 4);
      localHeader.writeUInt16LE(0, 6);
      localHeader.writeUInt16LE(0, 8);
      localHeader.writeUInt16LE(0, 10);
      localHeader.writeUInt16LE(0, 12);
      localHeader.writeUInt32LE(crc, 14);
      localHeader.writeUInt32LE(buf.length, 18);
      localHeader.writeUInt32LE(buf.length, 22);
      localHeader.writeUInt16LE(nameBuf.length, 26);
      localHeader.writeUInt16LE(0, 28);
      this.localChunks.push(localHeader, nameBuf, buf);
      const cd = Buffer.alloc(46);
      cd.writeUInt32LE(0x02014b50, 0);
      cd.writeUInt16LE(20, 4);
      cd.writeUInt16LE(20, 6);
      cd.writeUInt16LE(0, 8);
      cd.writeUInt16LE(0, 10);
      cd.writeUInt16LE(0, 12);
      cd.writeUInt16LE(0, 14);
      cd.writeUInt32LE(crc, 16);
      cd.writeUInt32LE(buf.length, 20);
      cd.writeUInt32LE(buf.length, 24);
      cd.writeUInt16LE(nameBuf.length, 28);
      cd.writeUInt16LE(0, 30);
      cd.writeUInt16LE(0, 32);
      cd.writeUInt16LE(0, 34);
      cd.writeUInt16LE(0, 36);
      cd.writeUInt32LE(0, 38);
      cd.writeUInt32LE(this.offset, 42);
      this.centralChunks.push(cd, nameBuf);
      this.offset += localHeader.length + nameBuf.length + buf.length;
      this.count++;
    }
    build() {
      const localBlock = Buffer.concat(this.localChunks);
      const centralBlock = Buffer.concat(this.centralChunks);
      const cdSize = centralBlock.length;
      const cdOffset = this.offset;
      const n = this.count;
      const eocd = Buffer.alloc(22);
      eocd.writeUInt32LE(0x06054b50, 0);
      eocd.writeUInt16LE(0, 4);
      eocd.writeUInt16LE(0, 6);
      eocd.writeUInt16LE(n, 8);
      eocd.writeUInt16LE(n, 10);
      eocd.writeUInt32LE(cdSize, 12);
      eocd.writeUInt32LE(cdOffset, 16);
      eocd.writeUInt16LE(0, 20);
      return Buffer.concat([localBlock, centralBlock, eocd]);
    }
  }

  // -- Inline Formatting Parser (Recursive Tokenizer) --
  // Handles nested/overlapping formats: **bold `code`**, **[link](url)**, etc.
  // Priorities: `code` (highest, no interior formatting) > **bold** > *italic* > [link](url) > plain text
  function parseInlineRuns(raw) {
    const runs = [];
    let i = 0;

    while (i < raw.length) {
      // Backtick code span — no interior formatting
      if (raw[i] === '`') {
        const end = raw.indexOf('`', i + 1);
        if (end !== -1) {
          runs.push({ text: raw.slice(i + 1, end), mono: true });
          i = end + 1;
          continue;
        }
      }

      // Bold — recursive interior parsing
      if (raw[i] === '*' && raw[i + 1] === '*') {
        let end = raw.indexOf('**', i + 2);
        // If no closing **, try matching at line end (treat as plain text)
        if (end === -1) {
          runs.push({ text: raw.slice(i, i + 2) });
          i += 2;
          continue;
        }
        const inner = raw.slice(i + 2, end);
        const innerRuns = parseInlineRuns(inner);
        for (const r of innerRuns) {
          r.bold = true;
          runs.push(r);
        }
        i = end + 2;
        continue;
      }

      // Italic (single *, not **)
      if (raw[i] === '*' && raw[i + 1] !== '*') {
        const end = raw.indexOf('*', i + 1);
        if (end !== -1) {
          const inner = raw.slice(i + 1, end);
          const innerRuns = parseInlineRuns(inner);
          for (const r of innerRuns) {
            r.italic = true;
            runs.push(r);
          }
          i = end + 1;
          continue;
        }
      }

      // Link [text](url) — recursive interior parsing for link text
      if (raw[i] === '[') {
        const closeBracket = raw.indexOf('](', i + 1);
        if (closeBracket !== -1) {
          const closeParen = raw.indexOf(')', closeBracket + 2);
          if (closeParen !== -1) {
            const linkText = raw.slice(i + 1, closeBracket);
            const linkUrl = raw.slice(closeBracket + 2, closeParen);
            const innerRuns = parseInlineRuns(linkText);
            for (const r of innerRuns) {
              r.link = linkUrl;
              runs.push(r);
            }
            i = closeParen + 1;
            continue;
          }
        }
      }

      // Plain text — consume until next special character
      let end = i + 1;
      while (end < raw.length && '`*['.indexOf(raw[end]) === -1) { end++; }
      runs.push({ text: raw.slice(i, end) });
      i = end;
    }

    if (runs.length === 0) { runs.push({ text: raw }); }
    return runs;
  }

  // -- Markdown to AST Parser --
  function parseMarkdownAST(md) {
    const lines = md.split('\n');
    const doc = { blocks: [] };
    let i = 0;
    while (i < lines.length) {
      let line = lines[i].trim();
      if (line === '---') { doc.blocks.push({ type: 'slide_break' }); i++; continue; }
      if (line.startsWith('#')) {
        const level = line.match(/^#+/)[0].length;
        doc.blocks.push({ type: 'heading', level, text: line.replace(/^#+\s*/, '') });
        i++; continue;
      }
      if (line.startsWith('```')) {
        const lang = line.replace('```', '').trim();
        i++;
        const code = [];
        while (i < lines.length && !lines[i].startsWith('```')) { code.push(lines[i]); i++; }
        doc.blocks.push({ type: 'code', lang, code: code.join('\n') });
        i++; continue;
      }
      if (line.startsWith('- ')) {
        const items = [];
        while (i < lines.length && lines[i].startsWith('- ')) { items.push(lines[i].slice(2)); i++; }
        doc.blocks.push({ type: 'list', items }); continue;
      }
      if (/^\d+\.\s/.test(line)) {
        const items = [];
        while (i < lines.length && /^\d+\.\s/.test(lines[i])) { items.push(lines[i].replace(/^\d+\.\s+/, '')); i++; }
        doc.blocks.push({ type: 'ordered_list', items }); continue;
      }
      if (line.includes('|') && lines[i + 1]?.includes('|')) {
        const rows = [];
        while (i < lines.length && lines[i].includes('|')) {
          const cells = lines[i].split('|').map(c => c.trim()).filter(Boolean);
          // Separator row check: each cell matches :?---+:?  and we only have the header row so far
          const isSeparator = cells.length > 0 && cells.every(c => /^:?-+:?$/.test(c)) && rows.length === 1;
          if (!isSeparator) { rows.push(cells); }
          i++;
        }
        doc.blocks.push({ type: 'table', rows }); continue;
      }
      if (line.startsWith('> ')) {
        const items = [];
        while (i < lines.length && lines[i].startsWith('> ')) { items.push(lines[i].slice(2)); i++; }
        doc.blocks.push({ type: 'blockquote', items }); continue;
      }
      if (line.startsWith('![')) {
        // Extended image syntax: ![alt](url) or ![alt](url){w=N,h=N} or ![alt](url){50%}
        const imgMatch = line.match(/!\[([^\]]*)\]\((.+?)\)(?:\{([^}]+)\})?/);
        if (imgMatch) {
          const block = { type: 'image', data: imgMatch[2] };
          const dimStr = imgMatch[3];
          if (dimStr) {
            if (dimStr.includes('%')) {
              block.widthPct = parseFloat(dimStr);
            } else {
              const wMatch = dimStr.match(/w=(\d+)/);
              const hMatch = dimStr.match(/h=(\d+)/);
              if (wMatch) block.width = parseInt(wMatch[1]);
              if (hMatch) block.height = parseInt(hMatch[1]);
            }
          }
          doc.blocks.push(block);
        }
        i++; continue;
      }
      if (line) {
        const runs = parseInlineRuns(line);
        doc.blocks.push({ type: 'paragraph', text: line, runs });
      }
      i++;
    }
    return doc;
  }

  // -- Slide IR Builder --
  const SLIDE_W = 9144000;
  const SLIDE_H = 6858000;
  const CHARS_PER_LINE = 80;
  const LINE_HEIGHT = 300000;

  // CJK-aware character-width estimation.
  // CJK/wide characters take ~2× the width of Latin chars in proportional fonts.
  // Used instead of raw .length for accurate line-count estimation.
  function cjkWeightedLen(text) {
    let w = 0;
    for (const ch of text) {
      const code = ch.charCodeAt(0);
      if ((code >= 0x4E00 && code <= 0x9FFF)   // CJK Unified Ideographs
       || (code >= 0x3400 && code <= 0x4DBF)   // CJK Extension A
       || (code >= 0xF900 && code <= 0xFAFF)   // CJK Compatibility
       || (code >= 0x3040 && code <= 0x30FF)   // Hiragana + Katakana
       || (code >= 0xAC00 && code <= 0xD7AF)   // Hangul Syllables
       || (code >= 0xFF01 && code <= 0xFF60)   // Fullwidth Forms
       || (code >= 0xFFE0 && code <= 0xFFE6)) { // Fullwidth Signs
        w += 2;
      } else {
        w += 1;
      }
    }
    return w;
  }

  function estimateBlockHeight(b) {
    if (b.type === 'list' || b.type === 'ordered_list') {
      let h = 0;
      for (const item of (b.items || [])) { const lines = Math.ceil(cjkWeightedLen(item) / CHARS_PER_LINE) || 1; h += lines * LINE_HEIGHT; }
      return h;
    }
    if (b.type === 'blockquote') {
      let h = 0;
      for (const item of (b.items || [])) { const lines = Math.ceil(cjkWeightedLen(item) / CHARS_PER_LINE) || 1; h += lines * LINE_HEIGHT; }
      return h;
    }
    if (b.type === 'paragraph') { const text = b.text || ''; const lines = Math.ceil(cjkWeightedLen(text) / CHARS_PER_LINE) || 1; return lines * LINE_HEIGHT; }
    if (b.type === 'code') { const codeLines = (b.code || '').split('\n').length; return Math.max(codeLines * 250000, 1500000); }
    if (b.type === 'table') { const rows = (b.rows || []).length || 1; return Math.max(rows * 400000, 2000000); }
    return 800000;
  }

  function estimateSlideH(slide) {
    let h = (slide.title ? 1000000 : 0) + (slide.subtitle ? 800000 : 0);
    for (const img of (slide.images || [])) {
      const data = (img && typeof img === 'object') ? img : { data: img };
      const imgW = data.width || (data.widthPct ? Math.floor(SLIDE_W * data.widthPct / 100) : (SLIDE_W - 2 * COL));
      const imgH = data.height || (data.width && !data.height ? Math.floor(imgW * 9 / 16) : Math.floor(imgW * 9 / 16));
      h += imgH + 100000;
    }
    for (const b of slide.blocks) { h += estimateBlockHeight(b); }
    return h;
  }

  function buildSlideIR(ast) {
    const slides = [];
    let current = createSlide();
    function createSlide() { return { title: '', subtitle: '', blocks: [], images: [], _overflow: false }; }
    function splitOverflow() {
      if (estimateSlideH(current) <= SLIDE_H) return;
      const overflow = createSlide();
      overflow._overflow = true;
      let h = (current.title ? 1000000 : 0) + (current.subtitle ? 800000 : 0);
      for (const img of (current.images || [])) {
        const data = (img && typeof img === 'object') ? img : { data: img };
        const imgW = data.width || (data.widthPct ? Math.floor(SLIDE_W * data.widthPct / 100) : (SLIDE_W - 2 * COL));
        const imgH = data.height || Math.floor(imgW * 9 / 16);
        h += imgH + 100000;
      }
      const keepBlocks = [], overflowBlocks = [];
      for (const b of current.blocks) {
        const bh = estimateBlockHeight(b);
        if (h + bh <= SLIDE_H) { keepBlocks.push(b); h += bh; }
        else { overflowBlocks.push(b); }
      }
      if (overflowBlocks.length > 0) { current.blocks = keepBlocks; overflow.blocks = overflowBlocks; slides.push(current); current = overflow; }
    }
    for (const b of ast.blocks) {
      if (b.type === 'heading' && b.level === 1) {
        if (current.title || current.blocks.length || current.images.length) { splitOverflow(); slides.push(current); }
        current = createSlide(); current.title = b.text; continue;
      }
      if (b.type === 'slide_break') { splitOverflow(); slides.push(current); current = createSlide(); continue; }
      if (b.type === 'heading' && b.level === 2) { current.subtitle = b.text; continue; }
      if (b.type === 'image') {
        // Store image as file path (NOT base64). Bytes read lazily by addImageToZip.
        current.images.push({ data: b.data, _ext: b._ext, width: b.width, height: b.height, widthPct: b.widthPct });
        continue;
      }
      current.blocks.push(b); splitOverflow();
    }
    splitOverflow(); slides.push(current); return slides;
  }

  // -- Layout Engine --
  const COL = SLIDE_W / 12;
  function grid(col, span, y, h) { return { x: col * COL, y, w: span * COL, h }; }

  function layoutSlide(slide) {
    const layout = [];
    if (slide.title) { layout.push({ type: 'title', text: slide.title, rect: grid(0, 12, 0, 1000000) }); }
    if (slide.subtitle) { layout.push({ type: 'subtitle', text: slide.subtitle, rect: grid(0, 12, 1000000, 800000) }); }
    let bodyStart = slide.title ? (slide.subtitle ? 1800000 : 1000000) : (slide.subtitle ? 1800000 : 0);
    const DEFAULT_IMG_W = SLIDE_W - 2 * COL;
    const DEFAULT_IMG_H = Math.floor(DEFAULT_IMG_W * 9 / 16);
    if (slide.images && slide.images.length > 0) {
      for (const img of slide.images) {
        const imgData = (img && typeof img === 'object') ? img : { data: img };
        let imgW, imgH;
        if (imgData.width && imgData.height) {
          imgW = imgData.width;
          imgH = imgData.height;
        } else if (imgData.widthPct) {
          imgW = Math.floor(SLIDE_W * imgData.widthPct / 100);
          imgH = Math.floor(imgW * 9 / 16);
        } else if (imgData.width && !imgData.height) {
          imgW = imgData.width;
          imgH = Math.floor(imgW * 9 / 16);
        } else {
          imgW = DEFAULT_IMG_W;
          imgH = DEFAULT_IMG_H;
        }
        const imgX = Math.floor((SLIDE_W - imgW) / 2);
        layout.push({ type: 'image', data: imgData.data, rect: { x: imgX, y: bodyStart, w: imgW, h: imgH } });
        bodyStart += imgH + 100000;
      }
    }
    let footerH = slide._showSlideNumber ? 600000 : 0;
    if (footerH > 0) {
      layout.push({ type: 'page_number', text: `${slide._pageNumber} / ${slide._totalPages}`, rect: grid(9, 3, SLIDE_H - footerH, 400000) });
    }
    let y = bodyStart;
    for (const b of slide.blocks) {
      if (b.type === 'paragraph') { const text = b.text || ''; const lines = Math.ceil(cjkWeightedLen(text) / CHARS_PER_LINE) || 1; const h = lines * LINE_HEIGHT; layout.push({ type: 'text', text: b.text, runs: b.runs, rect: grid(0, 12, y, h) }); y += h; }
      if (b.type === 'list') { for (const item of b.items) { const lines = Math.ceil(cjkWeightedLen(item) / CHARS_PER_LINE) || 1; const h = lines * LINE_HEIGHT; layout.push({ type: 'bullet', text: item, rect: grid(1, 11, y, h) }); y += h; } }
      if (b.type === 'ordered_list') { for (let idx = 0; idx < b.items.length; idx++) { const lines = Math.ceil(cjkWeightedLen(b.items[idx]) / CHARS_PER_LINE) || 1; const h = lines * LINE_HEIGHT; layout.push({ type: 'ordered_bullet', text: b.items[idx], number: idx + 1, rect: grid(1, 11, y, h) }); y += h; } }
      if (b.type === 'blockquote') { for (const item of b.items) { const lines = Math.ceil(cjkWeightedLen(item) / CHARS_PER_LINE) || 1; const h = lines * LINE_HEIGHT; layout.push({ type: 'blockquote', text: item, rect: grid(1, 11, y, h) }); y += h; } }
      if (b.type === 'code') { const codeLines = (b.code || '').split('\n').length; const h = Math.max(codeLines * 250000, 1500000); layout.push({ type: 'code', text: b.code, rect: grid(0, 12, y, h) }); y += h; }
      if (b.type === 'table') { const rows = (b.rows || []).length || 1; const h = Math.max(rows * 400000, 2000000); layout.push({ type: 'table', rows: b.rows, rect: grid(0, 12, y, h) }); y += h; }
    }
    return layout;
  }

  // -- Image Embedding (Direct Binary Pass-Through) --
  // DESIGN: Images flow through the pipeline as FILE PATHS (strings), NOT as
  // base64 data URIs.  File bytes are read ONLY at the final moment — directly
  // from disk into the ZIP via addImageToZip().  This avoids ~33% base64 bloat
  // and the corresponding memory/performance hit in Wasm environments when
  // encoding many large PNG charts.
  function resolveImages(ast) {
    for (const block of ast.blocks) {
      if (block.type === 'image') {
        const filePath = block.data;
        if (!filePath) { block.data = null; continue; }
        if (filePath.startsWith('data:')) {
          console.warn('WARNING: data URI image. Use direct file paths for better performance (avoid ~33% base64 bloat).');
          continue;
        }
        if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
          console.warn(`WARNING: Image "${filePath}" is a remote URL. Skipping.`);
          block.data = null; continue;
        }
        try {
          const resolvedPath = pathModule.resolve(filePath);
          fs.accessSync(resolvedPath);
          const ext = pathModule.extname(filePath).toLowerCase();
          block.data = resolvedPath;
          block._ext = ext.replace('.', '') || 'png';
        } catch (e) {
          console.warn(`WARNING: Cannot access "${filePath}": ${e.message}. Skipping.`);
          block.data = null;
        }
      }
    }
    ast.blocks = ast.blocks.filter(b => !(b.type === 'image' && b.data === null));
  }

  // Read image bytes from disk on demand — the ONLY place file bytes are read.
  function addImageToZip(zip, imgPath, imgExt) {
    const buf = fs.readFileSync(imgPath);
    const ext = imgExt || 'png';
    const name = nextImageName(ext);
    zip.add(`ppt/media/${name}`, buf);
    return { fileName: name, rId: nextRelId(), ext };
  }
  // -- Slide Rendering (OOXML Core) --
  function slideMaster() {
    return `<?xml version="1.0" encoding="UTF-8"?>
<p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
             xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <p:cSld>
    <p:bg>
      <p:bgRef idx="1001">
        <a:schemeClr val="bg1"/>
      </p:bgRef>
    </p:bg>
    <p:spTree>
      <p:nvGrpSpPr>
        <p:cNvPr id="1" name=""/>
        <p:cNvGrpSpPr/>
        <p:nvPr/>
      </p:nvGrpSpPr>
      <p:grpSpPr>
        <a:xfrm>
          <a:off x="0" y="0"/>
          <a:ext cx="0" cy="0"/>
          <a:chOff x="0" y="0"/>
          <a:chExt cx="0" cy="0"/>
        </a:xfrm>
      </p:grpSpPr>
    </p:spTree>
  </p:cSld>
  <p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
  <p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
  <p:txStyles>
    <p:titleStyle>
      <a:lvl1pPr algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPct val="0"/></a:spcBef>
        <a:buNone/>
        <a:defRPr sz="4400" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mj-lt"/>
          <a:ea typeface="+mj-ea"/>
          <a:cs typeface="+mj-cs"/>
        </a:defRPr>
      </a:lvl1pPr>
    </p:titleStyle>
    <p:bodyStyle>
      <a:lvl1pPr marL="228600" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="1000"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="2800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl1pPr>
      <a:lvl2pPr marL="685800" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="2400" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl2pPr>
      <a:lvl3pPr marL="1143000" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="2000" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl3pPr>
      <a:lvl4pPr marL="1600200" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl4pPr>
      <a:lvl5pPr marL="2057400" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl5pPr>
      <a:lvl6pPr marL="2514600" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl6pPr>
      <a:lvl7pPr marL="2971800" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl7pPr>
      <a:lvl8pPr marL="3429000" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl8pPr>
      <a:lvl9pPr marL="3886200" indent="-228600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:lnSpc><a:spcPct val="90000"/></a:lnSpc>
        <a:spcBef><a:spcPts val="500"/></a:spcBef>
        <a:buFont typeface="Arial" panose="020B0604020202020204" pitchFamily="34" charset="0"/>
        <a:buChar char="•"/>
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl9pPr>
    </p:bodyStyle>
    <p:otherStyle>
      <a:defPPr>
        <a:defRPr lang="en-US"/>
      </a:defPPr>
      <a:lvl1pPr marL="0" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl1pPr>
      <a:lvl2pPr marL="457200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl2pPr>
      <a:lvl3pPr marL="914400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl3pPr>
      <a:lvl4pPr marL="1371600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl4pPr>
      <a:lvl5pPr marL="1828800" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl5pPr>
      <a:lvl6pPr marL="2286000" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl6pPr>
      <a:lvl7pPr marL="2743200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl7pPr>
      <a:lvl8pPr marL="3200400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl8pPr>
      <a:lvl9pPr marL="3657600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
        <a:defRPr sz="1800" kern="1200">
          <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
          <a:latin typeface="+mn-lt"/>
          <a:ea typeface="+mn-ea"/>
          <a:cs typeface="+mn-cs"/>
        </a:defRPr>
      </a:lvl9pPr>
    </p:otherStyle>
  </p:txStyles>
</p:sldMaster>`;
  }
  function slideMasterRels() {
    return `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
</Relationships>`;
  }
  function slideLayout() {
    return `<?xml version="1.0" encoding="UTF-8"?>
<p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
             xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
             xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <p:cSld name="Default">
    <p:spTree>
      <p:nvGrpSpPr>
        <p:cNvPr id="1" name=""/>
        <p:cNvGrpSpPr/>
        <p:nvPr/>
      </p:nvGrpSpPr>
      <p:grpSpPr>
        <a:xfrm>
          <a:off x="0" y="0"/>
          <a:ext cx="0" cy="0"/>
          <a:chOff x="0" y="0"/>
          <a:chExt cx="0" cy="0"/>
        </a:xfrm>
      </p:grpSpPr>
    </p:spTree>
  </p:cSld>
  <p:clrMapOvr>
    <a:masterClrMapping/>
  </p:clrMapOvr>
</p:sldLayout>`;
  }
  function slideLayoutRels() {
    return `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
</Relationships>`;
  }
    function theme() {
        return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
  <a:themeElements>
    <a:clrScheme name="Office">
      <a:dk1><a:srgbClr val="000000"/></a:dk1>
      <a:lt1><a:srgbClr val="FFFFFF"/></a:lt1>
      <a:dk2><a:srgbClr val="44546A"/></a:dk2>
      <a:lt2><a:srgbClr val="E7E6E6"/></a:lt2>
      <a:accent1><a:srgbClr val="4472C4"/></a:accent1>
      <a:accent2><a:srgbClr val="ED7D31"/></a:accent2>
      <a:accent3><a:srgbClr val="A5A5A5"/></a:accent3>
      <a:accent4><a:srgbClr val="FFC000"/></a:accent4>
      <a:accent5><a:srgbClr val="5B9BD5"/></a:accent5>
      <a:accent6><a:srgbClr val="70AD47"/></a:accent6>
      <a:hlink><a:srgbClr val="0563C1"/></a:hlink>
      <a:folHlink><a:srgbClr val="954F72"/></a:folHlink>
    </a:clrScheme>
    <a:fontScheme name="Office">
      <a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface="Microsoft YaHei Light"/><a:cs typeface="Calibri Light"/></a:majorFont>
      <a:minorFont><a:latin typeface="Calibri"/><a:ea typeface="Microsoft YaHei"/><a:cs typeface="Calibri"/></a:minorFont>
    </a:fontScheme>
    <a:fmtScheme name="Office">
      <a:fillStyleLst>
        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
        <a:gradFill rotWithShape="1">
          <a:gsLst>
            <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
            <a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
            <a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
          </a:gsLst>
          <a:lin ang="16200000" scaled="1"/>
        </a:gradFill>
        <a:gradFill rotWithShape="1">
          <a:gsLst>
            <a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/><a:satMod val="130000"/></a:schemeClr></a:gs>
            <a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/><a:satMod val="130000"/></a:schemeClr></a:gs>
            <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="94000"/><a:satMod val="135000"/></a:schemeClr></a:gs>
          </a:gsLst>
          <a:lin ang="16200000" scaled="0"/>
        </a:gradFill>
      </a:fillStyleLst>
      <a:lnStyleLst>
        <a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/><a:satMod val="105000"/></a:schemeClr></a:solidFill><a:prstDash val="solid"/></a:ln>
        <a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>
        <a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln>
      </a:lnStyleLst>
      <a:effectStyleLst>
        <a:effectStyle><a:effectLst/></a:effectStyle>
        <a:effectStyle><a:effectLst/></a:effectStyle>
        <a:effectStyle><a:effectLst/></a:effectStyle>
      </a:effectStyleLst>
      <a:bgFillStyleLst>
        <a:solidFill><a:schemeClr val="phClr"/></a:solidFill>
        <a:gradFill rotWithShape="1">
          <a:gsLst>
            <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/><a:satMod val="350000"/></a:schemeClr></a:gs>
            <a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/><a:satMod val="350000"/><a:shade val="99000"/></a:schemeClr></a:gs>
            <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="20000"/><a:satMod val="255000"/></a:schemeClr></a:gs>
          </a:gsLst>
          <a:path path="circle"><a:fillToRect l="50000" t="-80000" r="50000" b="180000"/></a:path>
        </a:gradFill>
        <a:gradFill rotWithShape="1">
          <a:gsLst>
            <a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/><a:satMod val="300000"/></a:schemeClr></a:gs>
            <a:gs pos="100000"><a:schemeClr val="phClr"><a:shade val="30000"/><a:satMod val="200000"/></a:schemeClr></a:gs>
          </a:gsLst>
          <a:path path="circle"><a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>
        </a:gradFill>
      </a:bgFillStyleLst>
    </a:fmtScheme>
  </a:themeElements>
  <a:objectDefaults/>
  <a:extraClrSchemeLst/>
</a:theme>`;
    }

  function renderRuns(runs, baseFontSize) {
    let body = '';
    for (const run of runs) {
      let rPrAttrs = baseFontSize ? ` sz="${baseFontSize}"` : '';
      let rPrChildren = '';
      let needsClose = false;
      if (run.bold) rPrAttrs += ' b="1"';
      if (run.italic) rPrAttrs += ' i="1"';
      if (run.mono) { rPrChildren += `<a:solidFill><a:srgbClr val="C7254E"/></a:solidFill><a:latin typeface="Consolas"/>`; needsClose = true; }
      if (run.link) {
        const linkRId = run._linkRId || '';
        rPrAttrs += ' u="sng"';
        rPrChildren += `<a:solidFill><a:srgbClr val="0563C1"/></a:solidFill><a:uLn><a:solidFill><a:srgbClr val="0563C1"/></a:solidFill></a:uLn><a:latin typeface="Calibri"/>`;
        rPrChildren += `<a:hlinkClick r:id="${linkRId}" tooltip="${esc(run.link)}"/>`;
        needsClose = true;
      }
      const rPrOpen = needsClose ? `<a:rPr${rPrAttrs}>${rPrChildren}` : `<a:rPr${rPrAttrs}/>`;
      const rPrClose = needsClose ? `</a:rPr>` : '';
      const runBody = `<a:t>${esc(run.text)}</a:t>`;
      body += `<a:r>${rPrOpen}${rPrClose}${runBody}</a:r>`;
    }
    return body;
  }

  function scanAndAssignLinks(slide, layout) {
    slide._linkMap = {};
    for (const item of layout) {
      const runs = item.runs || (item.text ? parseInlineRuns(item.text) : []);
      for (const run of runs) { if (run.link && !slide._linkMap[run.link]) { slide._linkMap[run.link] = nextRelId(); } }
    }
    for (const item of layout) { if (item.runs) { for (const run of item.runs) { if (run.link) run._linkRId = slide._linkMap[run.link]; } } }
  }

  function renderSlideXML(slide, layout) {
    let shapes = '';
    for (const item of layout) {
      if (item.type === 'title' || item.type === 'subtitle' || item.type === 'text' || item.type === 'bullet') {
        const r = item.rect;
        const fontSize = item.type === 'title' ? 3600 : 1800;
        const runs = item.runs || parseInlineRuns(item.text);
        const isBullet = item.type === 'bullet';
        const bulletChar = isBullet ? `<a:buChar char="\u2022"/>` : '';
        const runsXml = renderRuns(runs, fontSize);
        shapes += `
  <p:sp>
    <p:nvSpPr>
      <p:cNvPr id="${nextShapeId()}" name="${item.type}"/>
      <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
      <p:nvPr/>
    </p:nvSpPr>
    <p:spPr>
      <a:xfrm>
        <a:off x="${r.x}" y="${r.y}"/>
        <a:ext cx="${r.w}" cy="${r.h}"/>
      </a:xfrm>
    </p:spPr>
    <p:txBody>
      <a:bodyPr/>
      <a:lstStyle/>
      <a:p><a:pPr>${bulletChar}</a:pPr>${runsXml}</a:p>
    </p:txBody>
  </p:sp>`;
      }
      if (item.type === 'page_number') {
        const r = item.rect;
        shapes += `
  <p:sp>
    <p:nvSpPr>
      <p:cNvPr id="${nextShapeId()}" name="pageNumber"/>
      <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
      <p:nvPr/>
    </p:nvSpPr>
    <p:spPr>
      <a:xfrm>
        <a:off x="${r.x}" y="${r.y}"/>
        <a:ext cx="${r.w}" cy="${r.h}"/>
      </a:xfrm>
    </p:spPr>
    <p:txBody>
      <a:bodyPr/>
      <a:lstStyle/>
      <a:p><a:pPr algn="r"/><a:r><a:rPr sz="1200"><a:solidFill><a:srgbClr val="888888"/></a:solidFill></a:rPr><a:t>${esc(item.text)}</a:t></a:r></a:p>
    </p:txBody>
  </p:sp>`;
      }
      if (item.type === 'ordered_bullet') {
        const r = item.rect;
        const runsXml = renderRuns(parseInlineRuns(item.text), 1800);
        shapes += `
  <p:sp>
    <p:nvSpPr>
      <p:cNvPr id="${nextShapeId()}" name="ordered"/>
      <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
      <p:nvPr/>
    </p:nvSpPr>
    <p:spPr>
      <a:xfrm>
        <a:off x="${r.x}" y="${r.y}"/>
        <a:ext cx="${r.w}" cy="${r.h}"/>
      </a:xfrm>
    </p:spPr>
    <p:txBody>
      <a:bodyPr/>
      <a:lstStyle/>
      <a:p><a:pPr><a:buAutoNum type="arabicPeriod"/></a:pPr>${runsXml}</a:p>
    </p:txBody>
  </p:sp>`;
      }
      if (item.type === 'blockquote') {
        const r = item.rect;
        const runs = parseInlineRuns(item.text);
        // Note: blockquote no longer forces italic — preserves user formatting intent
        const runsXml = renderRuns(runs, 1600);
        shapes += `
  <p:sp>
    <p:nvSpPr>
      <p:cNvPr id="${nextShapeId()}" name="blockquote"/>
      <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
      <p:nvPr/>
    </p:nvSpPr>
    <p:spPr>
      <a:xfrm>
        <a:off x="${r.x}" y="${r.y}"/>
        <a:ext cx="${r.w}" cy="${r.h}"/>
      </a:xfrm>
      <a:ln w="25400">
        <a:solidFill><a:srgbClr val="4472C4"/></a:solidFill>
      </a:ln>
    </p:spPr>
    <p:txBody>
      <a:bodyPr lIns="228600"/>
      <a:lstStyle/>
      <a:p>${runsXml}</a:p>
    </p:txBody>
  </p:sp>`;
      }
      if (item.type === 'code') {
        const r = item.rect;
        const lines = item.text.split('\n');
        let paragraphs = '';
        for (const line of lines) {
          paragraphs += `<a:p><a:r><a:rPr sz="1200"><a:solidFill><a:srgbClr val="333333"/></a:solidFill></a:rPr><a:t>${esc(line)}</a:t></a:r></a:p>`;
        }
        shapes += `
  <p:sp>
    <p:nvSpPr>
      <p:cNvPr id="${nextShapeId()}" name="code"/>
      <p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr>
      <p:nvPr/>
    </p:nvSpPr>
    <p:spPr>
      <a:xfrm>
        <a:off x="${r.x}" y="${r.y}"/>
        <a:ext cx="${r.w}" cy="${r.h}"/>
      </a:xfrm>
      <a:solidFill>
        <a:srgbClr val="F5F5F5"/>
      </a:solidFill>
    </p:spPr>
    <p:txBody>
      <a:bodyPr lIns="91440" tIns="45720" rIns="91440" bIns="45720"/>
      <a:lstStyle/>
      ${paragraphs}
    </p:txBody>
  </p:sp>`;
      }
      if (item.type === 'table') {
        const r = item.rect;
        const rows = item.rows;
        const numCols = rows[0] ? rows[0].length : 1;
        const colWidth = Math.floor(r.w / numCols);
        const rowHeight = Math.floor(r.h / rows.length);
        let gridCols = '';
        for (let c = 0; c < numCols; c++) { gridCols += `<a:gridCol w="${colWidth}"/>`; }
        let tableRows = '';
        for (let ri = 0; ri < rows.length; ri++) {
          let cells = '';
          const isHeader = ri === 0;
          for (let ci = 0; ci < (rows[ri] ? rows[ri].length : 0); ci++) {
            if (isHeader) {
              cells += `<a:tc>
                <a:txBody>
                  <a:bodyPr/>
                  <a:lstStyle/>
                  <a:p><a:r><a:rPr sz="1400" b="1"><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill></a:rPr><a:t>${esc(rows[ri][ci])}</a:t></a:r></a:p>
                </a:txBody>
                <a:tcPr><a:solidFill><a:srgbClr val="4472C4"/></a:solidFill></a:tcPr>
              </a:tc>`;
            } else {
              cells += `<a:tc>
                <a:txBody>
                  <a:bodyPr/>
                  <a:lstStyle/>
                  <a:p><a:r><a:rPr sz="1400"/><a:t>${esc(rows[ri][ci])}</a:t></a:r></a:p>
                </a:txBody>
                <a:tcPr/>
              </a:tc>`;
            }
          }
          tableRows += `<a:tr h="${rowHeight}">${cells}</a:tr>`;
        }
        shapes += `
  <p:graphicFrame>
    <p:nvGraphicFramePr>
      <p:cNvPr id="${nextShapeId()}" name="table"/>
      <p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr>
      <p:nvPr/>
    </p:nvGraphicFramePr>
    <p:xfrm>
      <a:off x="${r.x}" y="${r.y}"/>
      <a:ext cx="${r.w}" cy="${r.h}"/>
    </p:xfrm>
    <a:graphic>
      <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
        <a:tbl>
          <a:tblPr/>
          <a:tblGrid>${gridCols}</a:tblGrid>
          ${tableRows}
        </a:tbl>
      </a:graphicData>
    </a:graphic>
  </p:graphicFrame>`;
      }
      if (item.type === 'image' && item.imageRId) {
        const r = item.rect;
        shapes += `
  <p:pic>
    <p:nvPicPr>
      <p:cNvPr id="${nextShapeId()}" name="image"/>
      <p:cNvPicPr/>
      <p:nvPr/>
    </p:nvPicPr>
    <p:blipFill>
      <a:blip r:embed="${item.imageRId}"/>
      <a:stretch>
        <a:fillRect/>
      </a:stretch>
    </p:blipFill>
    <p:spPr>
      <a:xfrm>
        <a:off x="${r.x}" y="${r.y}"/>
        <a:ext cx="${r.w}" cy="${r.h}"/>
      </a:xfrm>
      <a:prstGeom prst="rect">
        <a:avLst/>
      </a:prstGeom>
    </p:spPr>
  </p:pic>`;
      }
    }
    return `<?xml version="1.0" encoding="UTF-8"?>
<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
 xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<p:cSld>
  <p:spTree>
    <p:nvGrpSpPr>
      <p:cNvPr id="1" name=""/>
      <p:cNvGrpSpPr/>
      <p:nvPr/>
    </p:nvGrpSpPr>
    <p:grpSpPr>
      <a:xfrm>
        <a:off x="0" y="0"/>
        <a:ext cx="0" cy="0"/>
        <a:chOff x="0" y="0"/>
        <a:chExt cx="0" cy="0"/>
      </a:xfrm>
    </p:grpSpPr>
    ${shapes}
  </p:spTree>
</p:cSld>
<p:clrMapOvr>
  <a:masterClrMapping/>
</p:clrMapOvr>
</p:sld>`;
  }

  // -- OOXML Template Functions --
  function rootRels() {
    return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>`;
  }

  function presentation(slideCount) {
    let slideList = '';
    for (let i = 0; i < slideCount; i++) { slideList += `    <p:sldId id="${256 + i}" r:id="rId${i + 2}"/>\n`; }
    return `<?xml version="1.0" encoding="UTF-8"?>
<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
                xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
                xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <p:sldMasterIdLst>
    <p:sldMasterId id="2147483648" r:id="rId1"/>
  </p:sldMasterIdLst>
  <p:sldIdLst>
${slideList}  </p:sldIdLst>
  <p:sldSz cx="9144000" cy="6858000" type="screen4x3"/>
  <p:notesSz cx="6858000" cy="9144000"/>
  <p:defaultTextStyle>
    <a:defPPr>
      <a:defRPr lang="en-US"/>
    </a:defPPr>
    <a:lvl1pPr marL="0" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl1pPr>
    <a:lvl2pPr marL="457200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl2pPr>
    <a:lvl3pPr marL="914400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl3pPr>
    <a:lvl4pPr marL="1371600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl4pPr>
    <a:lvl5pPr marL="1828800" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl5pPr>
    <a:lvl6pPr marL="2286000" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl6pPr>
    <a:lvl7pPr marL="2743200" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl7pPr>
    <a:lvl8pPr marL="3200400" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl8pPr>
    <a:lvl9pPr marL="3657600" algn="l" defTabSz="914400" rtl="0" eaLnBrk="1" latinLnBrk="0" hangingPunct="1">
      <a:defRPr sz="1800" kern="1200">
        <a:solidFill><a:schemeClr val="tx1"/></a:solidFill>
        <a:latin typeface="+mn-lt"/>
        <a:ea typeface="+mn-ea"/>
        <a:cs typeface="+mn-cs"/>
      </a:defRPr>
    </a:lvl9pPr>
  </p:defaultTextStyle>
</p:presentation>`;
  }

  function presentationRels(slideCount) {
    let rels = `  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>\n`;
    for (let i = 0; i < slideCount; i++) { rels += `  <Relationship Id="rId${i + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide${i + 1}.xml"/>\n`; }
    const presRId = slideCount + 2;
    rels += `  <Relationship Id="rId${presRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" Target="presProps.xml"/>\n`;
    rels += `  <Relationship Id="rId${presRId + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps" Target="viewProps.xml"/>\n`;
    rels += `  <Relationship Id="rId${presRId + 2}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>\n`;
    rels += `  <Relationship Id="rId${presRId + 3}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles" Target="tableStyles.xml"/>\n`;
    return `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
${rels}</Relationships>`;
  }

  function slideRels(slide) {
    let rels = `  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>\n`;
    if (slide._images) { for (const img of slide._images) { rels += `  <Relationship Id="${img.rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/${img.fileName}"/>\n`; } }
    if (slide._linkMap) { for (const [url, rId] of Object.entries(slide._linkMap)) { rels += `  <Relationship Id="${rId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${esc(url)}" TargetMode="External"/>\n`; } }
    return `<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
${rels}</Relationships>`;
  }

  function contentTypes(slideCount, imageExts) {
    let defaults = `  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>\n`;
    defaults += `  <Default Extension="xml" ContentType="application/xml"/>\n`;
    if (imageExts && imageExts.size > 0) {
      for (const ext of imageExts) {
        defaults += `  <Default Extension="${ext}" ContentType="image/${ext}"/>\n`;
      }
    }
    let overrides = '';
    overrides += `  <Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>\n`;
    overrides += `  <Override PartName="/ppt/presProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"/>\n`;
    overrides += `  <Override PartName="/ppt/viewProps.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"/>\n`;
    overrides += `  <Override PartName="/ppt/tableStyles.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"/>\n`;
    overrides += `  <Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>\n`;
    overrides += `  <Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>\n`;
    overrides += `  <Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>\n`;
    for (let i = 0; i < slideCount; i++) { overrides += `  <Override PartName="/ppt/slides/slide${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>\n`; }
    overrides += `  <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>\n`;
    overrides += `  <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>\n`;
    return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
${defaults}${overrides}</Types>`;
  }

  function presProps() { return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:presentationPr xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
                  xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
                  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <p:extLst>
    <p:ext uri="{E76CE94A-603C-4142-B9EB-6D1370010A27}">
      <p14:discardImageEditData xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="0"/>
    </p:ext>
    <p:ext uri="{D31A062A-798A-432F-ABDD-D45BAFD82E40}">
      <p14:defaultImageDpi xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="220"/>
    </p:ext>
    <p:ext uri="{FD5C536A-5214-4C46-B8D8-A1DB69A19C07}">
      <p14:chartTrackingRefBased xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" val="1"/>
    </p:ext>
  </p:extLst>
</p:presentationPr>`; }

  function viewProps() {
    return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p:viewPr xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
          xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
          xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  <p:normalViewPr showOutlineIcons="1">
    <p:restoredLeft sz="15600"/>
    <p:restoredTop sz="15260"/>
  </p:normalViewPr>
  <p:slideViewPr showComments="1">
    <p:cSldViewPr snapToGrid="1" snapToObjects="1" showGuides="1">
      <p:cViewPr varScale="1">
        <p:scale>
          <a:sx n="100000" d="100000"/>
          <a:sy n="100000" d="100000"/>
        </p:scale>
        <p:origin x="0" y="0"/>
      </p:cViewPr>
      <p:guideLst/>
    </p:cSldViewPr>
  </p:slideViewPr>
  <p:notesTextViewPr>
    <p:cViewPr>
      <p:scale>
        <a:sx n="1" d="1"/>
        <a:sy n="1" d="1"/>
      </p:scale>
      <p:origin x="0" y="0"/>
    </p:cViewPr>
  </p:notesTextViewPr>
  <p:gridSpacing cx="76200" cy="76200"/>
</p:viewPr>`;
  }

  function tableStyles() { return `<?xml version="1.0" encoding="UTF-8"?>\n<a:tblStyleLst xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" def="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"/>`; }

  function docPropsCore() {
    const now = new Date().toISOString().replace(/\.\d{3}/, '');
    return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:creator>BoxAgnts</dc:creator><cp:lastModifiedBy>BoxAgnts</cp:lastModifiedBy><cp:revision>1</cp:revision><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`;
  }

  function docPropsApp(slideCount) {
    return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime><Words>0</Words><Application>BoxAgnts PPTX Generator</Application><PresentationFormat>On-screen Show (4:3)</PresentationFormat><Paragraphs>0</Paragraphs><Slides>${slideCount}</Slides><Notes>0</Notes><HiddenSlides>0</HiddenSlides><MMClips>0</MMClips><ScaleCrop>false</ScaleCrop><LinksUpToDate>false</LinksUpToDate><SharedDoc>false</SharedDoc><HyperlinksChanged>false</HyperlinksChanged><AppVersion>1.0</AppVersion></Properties>`;
  }

  // -- Main Compiler Pipeline --
  function compileMarkdownToPPTX(md, output_path, options) {
    if (!options) options = {};
    shapeId = 1; relId = 2; imageIndex = 1;

    const zip = new ZipBuilder();
    const ast = parseMarkdownAST(md);

    resolveImages(ast);

    const slides = buildSlideIR(ast);
    const totalSlides = slides.length;
    const imageExts = new Set();

    for (let i = 0; i < slides.length; i++) {
      const slide = slides[i];
      slide._images = [];
      slide._showSlideNumber = !!options.showSlideNumbers;
      slide._pageNumber = i + 1;
      slide._totalPages = totalSlides;
      if (slide.images && slide.images.length > 0) {
        for (const img of slide.images) {
          const imgData = (img && typeof img === 'object') ? img.data : img;
          if (imgData && typeof imgData === 'string' && !imgData.startsWith('data:')) {
            const ext = (img && img._ext) || 'png';
            const imgInfo = addImageToZip(zip, imgData, ext);
            slide._images.push(imgInfo);
            if (imgInfo.ext) imageExts.add(imgInfo.ext);
          }
        }
      }
    }

    zip.add("_rels/.rels", rootRels());
    zip.add("ppt/presentation.xml", presentation(slides.length));
    zip.add("ppt/_rels/presentation.xml.rels", presentationRels(slides.length));
    zip.add("ppt/presProps.xml", presProps());
    zip.add("ppt/viewProps.xml", viewProps());
    zip.add("ppt/tableStyles.xml", tableStyles());
    zip.add("ppt/slideMasters/slideMaster1.xml", slideMaster());
    zip.add("ppt/slideMasters/_rels/slideMaster1.xml.rels", slideMasterRels());
    zip.add("ppt/slideLayouts/slideLayout1.xml", slideLayout());
    zip.add("ppt/slideLayouts/_rels/slideLayout1.xml.rels", slideLayoutRels());
    zip.add("ppt/theme/theme1.xml", theme());

    zip.add("docProps/core.xml", docPropsCore());
    zip.add("docProps/app.xml", docPropsApp(slides.length));

    for (let i = 0; i < slides.length; i++) {
      const slide = slides[i];
      const layout = layoutSlide(slide);
      let imgIdx = 0;
      for (const item of layout) { if (item.type === 'image' && slide._images[imgIdx]) { item.imageRId = slide._images[imgIdx].rId; imgIdx++; } }
      scanAndAssignLinks(slide, layout);
      zip.add(`ppt/slides/slide${i + 1}.xml`, renderSlideXML(slide, layout));
      zip.add(`ppt/slides/_rels/slide${i + 1}.xml.rels`, slideRels(slide));
    }

    zip.add("[Content_Types].xml", contentTypes(slides.length, imageExts));

    fs.writeFileSync(output_path, zip.build());
    return { path: output_path, slides: slides.length };
  }

  // -- ENTRY POINT - DO NOT MODIFY --
  const md = `{{markdown}}`.replace(/^\{\{markdown\}\}$/, '') || '';
  const out = `{{output_path}}`.replace(/^\{\{output_path\}\}$/, '') || '/tmp/output.pptx';
  const showNums = (`{{show_slide_numbers}}`.replace(/^\{\{show_slide_numbers\}\}$/, '') || 'false') === 'true';

  const result = compileMarkdownToPPTX(md, out, { showSlideNumbers: showNums });
  console.log(`PPTX generated: ${result.path}`);
  console.log(`Slides: ${result.slides}`);
})();

Read the full file on GitHub · 1,510 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 5d ago First seen · 1,510 lines · 45 tokens per session scan A d11b9df5ef7e

Subscribe to this mod's changes

pptx-generator is a skill published in the GitHub repository guyoung/boxagnts (11 stars, last pushed 1mo ago), licensed MIT. It adds 45 tokens to every session and 21,925 once invoked, about $0.0002 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.

Related

Other skills, from other repositories

pptx

当涉及到 .pptx 文件的任何操作时使用此技能——无论是作为输入、输出还是两者兼有。包括:创建幻灯片、演示文稿或路演材料;读取、解析或提取任何 .pptx 文件中的文本(即使提取的内容将用于其他地方,如邮件或摘要);编辑、修改或更新现有演示文稿;合并或拆分幻灯片文件;处理模板、布局、演讲者备注或批注。当用户提到“演示文稿”、”幻灯片“、”PPT“或引用 .pptx 文件名时触发,无论他们之后打算如何使用内容。如果需要打开、创建或操作 .pptx 文件,就使用此技能。.

agentscope-ai/QwenPaw · 165 tokens

pdf

当用户需要对PDF文件进行任何操作时,请使用此技能。包括从 PDF 中读取或提取文本/表格、合并多个 PDF、拆分 PDF、旋转页面、添加水印、创建新PDF、填写PDF表单、加密/解密 PDF、提取图片,以及对扫描版 PDF 进行 OCR 使其可搜索。如果用户提到 .pdf 文件或要求生成 PDF,请使用此技能。.

agentscope-ai/QwenPaw · 95 tokens

nano-pdf

Edits PDF files using natural-language instructions via the nano-pdf CLI. Supports modifying text, changing titles, fixing typos, and updating content on specific pages. Use when the user wants to edit a PDF, modify PDF content, update PDF text, fix a typo in a PDF, change a PDF title, or rewrite part of a PDF page.

elizaOS/eliza · 75 tokens

pdf-toolkit

Structured .pdf operations: extract text/tables, merge pages from multiple PDFs, split a PDF by page ranges, fill PDF form fields, and generate fresh PDFs from JSON. Trigger when the user wants programmatic PDF work without natural-language rewriting — examples: pull tables from a report, combine three PDFs, extract…

opensquilla/opensquilla · 127 tokens

hive.pdf

Read, write, merge, split, rotate, watermark, encrypt, and OCR PDF files using Python (pypdf, pdfplumber, reportlab, pypdfium2) and command-line tools (poppler-utils, qpdf). Use when the user asks to extract text/tables/images from a PDF, create or modify a PDF, combine or split PDFs, OCR a scanned PDF…

aden-hive/hive · 98 tokens

sn-ppt-entry

Entry point for PPT generation. Asks the user to choose a mode (fast, standard, or creative), then collects role / audience / scene / pagecount as needed. For standard mode, also asks how images should be sourced (AI generation, web search, or none), whether charts should use AI-generated infographics or ECharts, and…

OpenSenseNova/SenseNova-Skills · 204 tokens