pptx-generator

pptx-generator is a skill for Claude Code 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 Code

Written for Claude Code: when-to-use in frontmatter.

Needs its repository: it reads a path above its own folder, which exists only inside the repository. The line is <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>.

Good fit Use it to generate presentation files from Markdown and embed images stored on the local computer.

Compare 6 skills from other repositories ↓
Install

Getting it into your agent

It runs from inside its repository, so the clone comes first — what it calls does not travel with the file alone.

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

Made for: Claude Code.

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/github.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/github.svg" alt="Measured on agentmods" height="20"></a>

Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.

agentmods 80×15 button for pptx-generator

Your own site · 80×15
<a href="https://agentmods.dev/skills/guyoung/boxagnts/pptx-generator"><img src="https://agentmods.dev/badge/skills/guyoung/boxagnts/pptx-generator.svg" alt="Reviewed on agentmods" width="80" 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. A grade says what 26 rules found in the file — not that it is safe.
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.1 $0.00045 $0.21925
Opus 5.5 $0.00018 $0.08770
Sonnet 5.5 $0.00009 $0.04385
Haiku 4.5 $0.00005 $0.02193

Measured 2mo ago against content hash d11b9df5ef7e, method: parsed. Prices are Anthropic first-party input rates as of 2026-10-07, 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 2mo 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. 2mo 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 3mo ago), licensed MIT. It adds 45 tokens to every session and 21,925 once invoked, about $0.0002 per session on Opus 5.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

pptx-generator compared with skills from other repositories

pptx-generator next to 6 skills from other repositories
ModTypeStarsLast pushLicenceScanTokensWorks with
pptx-generatorguyoung/boxagntsSkill113mo agoMITA45Claude Code
pdfagentscope-ai/QwenPawSkill37kyesterdayApache-2.0A95Claude Code, Codex
officecli-academic-paperiOfficeAI/OfficeCLISkill33k7d agoApache-2.0A141Claude Code, Codex
officecliiOfficeAI/OfficeCLISkill33k7d agoApache-2.0A56Claude Code, Codex
surfsense-dataMODSetter/SurfSenseSkill17kyesterdayApache-2.0A0Claude Code, Codex
surfsense-htmlMODSetter/SurfSenseSkill17kyesterdayApache-2.0A89Claude Code, Codex
hive.pdfaden-hive/hiveSkill11k4d agoApache-2.0A98Claude Code, Codex