# Hackmd To PPTX

> Use when converting HackMD/Marp markdown slides to PowerPoint presentations, when merging multiple HackMD/Marp decks into one combined deck, or when asked for 簡報去 AI 味 — rewriting an AI-assisted deck to sound naturally spoken.

- Skill: `william-yeh/hackmd-to-pptx` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add william-yeh/hackmd-to-pptx`
- Raw SKILL.md: https://api.skillmd.com/api/skills/william-yeh/hackmd-to-pptx/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: william-yeh (https://skillmd.com/u/william-yeh)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/william-yeh/hackmd-to-pptx

---


# HackMD to PowerPoint Converter

Convert HackMD/Marp-style markdown slides to professional PowerPoint presentations.

## Routing

| Request | Action |
|---|---|
| Convert a deck to `.pptx` | `scripts/convert.py` — see **Invocation** |
| Combine several decks | `scripts/merge.py` — see **Merging Multiple Decks** |
| Check a deck's structure | `scripts/validate_deck.py` — see **Validating a Deck** |
| Contains `簡報去 AI 味` | Read `references/mode-slide-de-ai.md` and follow it |

`簡報去 AI 味` is the explicit trigger for the rewrite mode; only that request routes there. Conversion and merge requests keep their existing behavior and never rewrite prose — a deck is only rewritten when the user asks for it by name.

## Invocation

### Basic Conversion

```bash
# Convert with explicit output name
uv run scripts/convert.py input.md output.pptx

# Output name defaults to input.pptx
uv run scripts/convert.py presentation.md
```

`---`/`----` lines inside ` ``` ` fenced code blocks are treated as code by both `convert.py` and `merge.py`, so markup samples don't split the deck. `~~~` fences are not recognized — see SYNTAX.md "Separators Inside Code Fences".

### Merging Multiple Decks

Combine several HackMD/Marp files into one deck where each input becomes a top-level section (`---`) and its slides become sub-slides (`----`):

```bash
uv run scripts/merge.py deck1.md deck2.md deck3.md -o merged.md
uv run scripts/merge.py deck1.md deck2.md -o merged.md --title "Combined Deck"
uv run scripts/merge.py deck1.md deck2.md -o merged.md --no-toc
```

The first input's frontmatter (theme, paginate, etc.) is preserved; `--title` overrides the `title:` field. Each deck's first H1 becomes the section title; if absent, the filename stem is used. Internal `---` sections inside an input deck are demoted to `----` (PowerPoint sections can't nest).

**Table of Contents (default on):** A TOC slide is emitted as the first section, listing every deck's title as a bullet. Its heading is the `--title` value, or deck 1's title if `--title` is absent. Pass `--no-toc` to suppress.

**Style preservation:** If deck 1 has a `<style>...</style>` block at the top of its body (between frontmatter and first heading), the block is lifted into the merged file's head verbatim. Style blocks in other input decks are discarded (same precedent as frontmatter).

**Synthetic cover slides:** Every input deck gets a synthetic `# Deck Title` cover slide prepended, even if the deck already starts with `# Title`. This guarantees PowerPoint section grouping works correctly regardless of how the input deck's first slide is structured (e.g. `# H1` + `## H2`).

**Input requirements:**
- Decks with content *before* their first H1 are rejected; remove the preamble or move the title to the top.
- Empty files are rejected.
- `---` lines inside fenced code blocks are preserved (see **Basic Conversion**).

### Validating a Deck

Check any deck — hand-written, merged, or rewritten — for structural problems the converter would otherwise accept silently:

```bash
uv run scripts/validate_deck.py deck.md
```

Exits `0` when clean, `1` on findings (printed to stderr), `2` when the deck can't be read or the arguments are wrong. A missing file never exits `1` — that code means the deck has problems, not that you mistyped a path. Two checks run on a single deck:

| Code | Meaning |
|---|---|
| `EDITORIAL_MARKER_IN_DECK` | `[需要案例]` reached audience-facing content; it belongs in the rewrite comparison only |
| `NOTE_NOT_TERMINAL` | A heading, table, or fence sits below `note:`. `parse_slide` folds everything after `note:` into the note, so that content is narrated instead of displayed — silent data loss the converter never reports |

To compare a rewrite against its source, use `validate_rewrite(before, after)`, which adds `PROTECTED_DATA_LOST`, `PROTECTED_DATA_INVENTED`, `QUOTATION_ALTERED`, and `QUOTATION_INVENTED`.

Coverage is partial by design: names, product names, non-numeric table cells, and ASCII `"…"` quotations are not machine-checked. A clean report means nothing was *found*, not that nothing was lost. See `references/mode-slide-de-ai.md`.

## Configuration

Place `config.json` or `config.yaml` in the same directory as the markdown file:

```json
{
  "colors": {
    "accent": "0891B2",
    "codeBlock": "F1F5F9",
    "syntaxKeyword": "7C3AED",
    "syntaxString": "059669",
    "syntaxComment": "6B7280",
    "syntaxNumber": "DC2626",
    "syntaxFunction": "2563EB",
    "syntaxType": "D97706"
  },
  "fonts": {
    "header": "Trebuchet MS",
    "body": "Calibri",
    "code": "Consolas"
  }
}
```

**Colors:** 6-digit hex without `#`, all keys optional. **Fonts:** system font names, all keys optional.

## HackMD `<style>` blocks

A `<style>...</style>` block placed at the top of the markdown (between frontmatter and the first heading) is translated to PowerPoint run properties. Per-slide `<style>` blocks deeper in the deck are ignored.

**Supported selectors** (matched on the *trailing* token, so `.reveal .slides h1` resolves the same as `h1`):

- bare `.reveal .slides` — slide-body default
- `h1`, `h2`, `h3` — slide titles
- `code`, `pre` — inline code only (fenced code blocks render at a fixed 11pt to fit their auto-sized textbox)
- `p`, `li`, `a`, `blockquote`, `table` — other body elements

**Supported properties:**

- `color`, `background-color`
- `font-size`, `font-family`, `font-weight`, `font-style`
- `text-align`, `text-decoration`

Everything else is silently dropped. Values may use `#RRGGBB`, `#RGB`, `rgb(...)`, or common named colors. Font sizes accept `px`, `pt`, or `em` (1em ≈ 16px).

**Contrast guard.** When a CSS `color` would fail WCAG AA (4.5:1) against the slide background, it is dropped and the run falls back to its default. The converter targets the default Office white background; if your slide master uses a dark background, set `colors.slideBg` in `config.json` so the guard sees the real background.



