- name
- pptx-generator
- description
- Compile Markdown into a valid Microsoft PowerPoint (.pptx) file using pure JavaScript. Pipeline: Markdown → AST → Slide IR → Layout IR → OOXML → ZIP. Images embedded from local files.
- when_to_use
- Use when generating PowerPoint (.pptx) files from Markdown input.
- tools
- jsexec, read, write, bash
- args
- [{"name":"output_path","description":"Absolute file path for the generated .pptx file","required":true},{"name":"markdown","description":"Markdown text input. Supports headings, lists, code, tables, blockquotes, inline formatting, links, images (local paths), and --- slide breaks","required":true},{"name":"show_slide_numbers","description":"When true, renders N / M page numbers at bottom-right","required":false}]
> **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)
```js
// ===== 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
// -- 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;
}
Auf GitHub ansehen