Render markdown
Turns a markdown file on disk into a small styled HTML page and opens it
in the user's default browser. Built for the moment someone wants to look
at a doc they just wrote, not edit it.
When to use
Use this whenever the user asks to "open in html", "preview the md",
"render this doc", "open as a webpage", or similar — including cases where
the user types something short like "open again" after we've previewed a
file once already. Also reach for it after writing a planning doc,
handoff, or checklist on the user's behalf — offering a one-line preview
is usually appreciated.
Don't reach for this if the user wants to publish the output, embed it in
something, or needs precise typography. This is a quick-look tool.
How
Two modes — pick based on whether the user wants to look or edit.
One-shot (static file)
Render once to an HTML file and open it. Right for "show me this doc" or
the trailing offer after writing a handoff/plan.
bun run ~/.claude/skills/render-markdown/scripts/render-md.ts <input.md> [out.html]
open <out.html>
Default output path is /tmp/preview.html. The script prints the output
path on stdout: OUT=$(bun run … input.md) && open $OUT.
Live editor (server)
Spin up a tiny local server with a CodeMirror editor + live preview.
Right for "let me edit this" or "open it so I can iterate."
bun run ~/.claude/skills/render-markdown/scripts/render-md.ts serve <input.md> [port]
# then open http://localhost:7780 (or the printed port)
UI: single pane that toggles between read (rendered preview iframe)
and raw (CodeMirror markdown source). Top-right has the mode toggle
(eye / pencil) and the theme toggle (sun / system / moon) separated by a
1px divider. Keyboard: Cmd-E flips modes.
Edits in raw mode autosave to the source file (350ms debounce). The
preview reloads on save and on mode-flip to live. Theme + mode persist
in localStorage. The editor has no external-change watcher yet, so if
something else edits the file while CodeMirror has it open, the next
autosave will clobber that change — reload the editor tab to pick up
external edits.
Shared rendering
Both modes share the same renderer, dispatched by file extension. The
output is a self-contained HTML page in one-shot mode; in server mode the
same page is served at /preview with the theme toggle hidden (the outer
editor page owns it).
.md — YAML frontmatter is stripped (so marked doesn't promote it
to a giant heading via setext rules), the body is rendered with GFM and
footnotes, code blocks are syntax-highlighted via highlight.js and get a
hover-to-copy button (copy-button.js, mermaid blocks skipped), and
```mermaid fenced blocks render to **inline SVG at build time** via
beautiful-mermaid — synchronous, no CDN, no client JS. Diagram colors
are passed as CSS variables (var(--bg), var(--fg), var(--accent),
…) matching the page's design tokens, so the light/dark toggle drives the
diagram live with no re-render. Supports flowchart/state/sequence/class/ER
/xy with standard Mermaid syntax; erDiagram and classDiagram make this
good for schema and architecture diagrams. An invalid diagram renders an
inline error block (and fails the one-shot build with a non-zero exit).
.yaml / .yml — rendered as a structured document: top-level keys
become sections, nested objects nest by heading level, lists of objects
render as numbered cards, multi-line scalars are treated as markdown.
.feature — Gherkin parsed with the official @cucumber/gherkin
parser and rendered semantically: Feature/Rule/Scenario/Background as
headed sections, steps with color-coded keywords (Given/When/Then),
data tables and Examples as real tables, doc strings as code blocks,
tags as chips, and comments (including a # backed-by: header) as muted
notes. Invalid Gherkin falls back to a syntax-highlighted code block so
the file stays legible and the parse error is easy to spot.
.design — a crisp decision note (one Y-statement clause per line)
with a fixed keyword vocabulary, rendered hierarchically with color-coded
keywords like Gherkin. Section keywords: Context: (forces), Choose:
(decision), Over: (alternatives rejected), Because: (rationale),
So: (consequences), Not: (non-goals). Key: value metadata rows
(Status, Backs, Lineage, Owner, Date, Supersedes) render as a
muted monospace header, # sets the title, - lines become clause
bullets, and ```mermaid blocks render to inline SVG as elsewhere.
File layout
The script is split across files in scripts/, inlined at render time via
bun's import x from "./foo.ext" with { type: "text" } syntax:
| File |
Role |
render-md.ts |
Markdown/YAML/Gherkin/Design → HTML assembly + serve subcommand (Bun.serve). Format chosen by extension in detectFormat. Mermaid → inline SVG via beautiful-mermaid. |
styles.css |
Prose design tokens, light/dark themes, code, tables, footnotes, mermaid SVG, copy button, in-preview theme toggle. |
theme-toggle.html |
Sun / monitor / moon toggle markup + persistence script (used inside the preview HTML). |
editor.html |
Server-mode UI: CodeMirror (via esm.sh import map), mode toggle, theme toggle, autosave. |
copy-button.js |
Hover-to-copy button injected onto every <pre> code block in the preview (mermaid blocks skipped). |
Edit any one concern in isolation; no build step. If you change
styles.css or editor.html, restart the server (bun run … serve) —
bun text imports are loaded once at startup.
Design tokens follow the prose-typography skill: 680px measure,
16px/1.6 body, h2 with 2.6em top margin, borders and inline-code
backgrounds derived from the text color at low alpha so the surface
themes coherently in both modes.
Conventions
- Default to one-shot mode unless the user asks to edit, iterate, or
serve. One-shot is cheaper and disposable.
- One-shot output path defaults to
/tmp/preview.html. Overwriting is
fine — this is throwaway preview output.
- Server mode defaults to port
7780. If that's in use, pass a free
port as the third argument. Don't try to "park" a server in the
background without telling the user — they need to know the URL.
- After opening, give a one-line confirmation (what file you rendered,
where it landed). Don't dump the HTML contents.
- If the user iterates on the source file and asks to "open again", just
re-run the script — the output path stays stable so the existing
browser tab refreshes.
Why a script instead of inline rendering
The frontmatter quirk (setext heading promotion when --- follows
key/value lines) is the kind of thing that bites every time someone
reimplements this inline with marked. Bundling the script means we fix
it once. The design tokens, dark mode, theme toggle, footnote handling,
syntax highlighting, mermaid wiring, and table/checkbox styling also
stay consistent across previews.
Dependencies
bun on PATH. The script imports marked, marked-footnote,
highlight.js, yaml, @cucumber/gherkin, @cucumber/messages, and
beautiful-mermaid — bun auto-installs them on first run. import … with { type: "text" } for the adjacent CSS/HTML/JS files requires bun ≥ 1.1.
open (macOS) for one-shot mode — substitute xdg-open on Linux or
start on Windows.
- Mermaid diagrams render server-side to inline SVG via
beautiful-mermaid,
so a doc with diagrams needs no network at view time and works
offline. Server mode still loads CodeMirror from esm.sh (with an import
map pinning shared CodeMirror deps to dedupe state across packages);
re-renders cache the bundles in the browser.
1---2name: render-markdown3description: Render a local markdown (.md), YAML (.yaml/.yml), Gherkin (.feature), or decision-note (.design) file to a styled HTML page — either one-shot (static file, open in browser) or as a live CodeMirror editor on a local server with read/raw mode toggle and autosave. Use whenever the user wants to preview such a file (handoff doc, README, design note, planning checklist, config YAML, a Cucumber/Gherkin .feature spec, a .design decision note, anything with YAML frontmatter or GFM task-list checkboxes) without launching a dev server or a heavy editor. Use the server mode when the user wants to iterate on the source live, switch between rendered and raw, or share a localhost preview. Triggers on phrases like "open this in html", "preview the md", "render this doc", "render the feature file", "preview the gherkin", "render the design note", "open as a webpage", "render and open", "open the handoff in html", "view this in browser", "edit this md live", "serve this markdown", "spin up the editor".4---56# Render markdown78Turns a markdown file on disk into a small styled HTML page and opens it9in the user's default browser. Built for the moment someone wants to *look*10at a doc they just wrote, not edit it.1112## When to use1314Use this whenever the user asks to "open in html", "preview the md",15"render this doc", "open as a webpage", or similar — including cases where16the user types something short like "open again" after we've previewed a17file once already. Also reach for it after writing a planning doc,18handoff, or checklist on the user's behalf — offering a one-line preview19is usually appreciated.2021Don't reach for this if the user wants to publish the output, embed it in22something, or needs precise typography. This is a quick-look tool.2324## How2526Two modes — pick based on whether the user wants to *look* or *edit*.2728### One-shot (static file)2930Render once to an HTML file and open it. Right for "show me this doc" or31the trailing offer after writing a handoff/plan.3233```bash34bun run ~/.claude/skills/render-markdown/scripts/render-md.ts <input.md> [out.html]35open <out.html>36```3738Default output path is `/tmp/preview.html`. The script prints the output39path on stdout: `OUT=$(bun run … input.md) && open $OUT`.4041### Live editor (server)4243Spin up a tiny local server with a CodeMirror editor + live preview.44Right for "let me edit this" or "open it so I can iterate."4546```bash47bun run ~/.claude/skills/render-markdown/scripts/render-md.ts serve <input.md> [port]48# then open http://localhost:7780 (or the printed port)49```5051UI: single pane that toggles between **read** (rendered preview iframe)52and **raw** (CodeMirror markdown source). Top-right has the mode toggle53(eye / pencil) and the theme toggle (sun / system / moon) separated by a541px divider. Keyboard: `Cmd-E` flips modes.5556Edits in raw mode autosave to the source file (350ms debounce). The57preview reloads on save and on mode-flip to live. Theme + mode persist58in `localStorage`. The editor has no external-change watcher yet, so if59something else edits the file while CodeMirror has it open, the next60autosave will clobber that change — reload the editor tab to pick up61external edits.6263### Shared rendering6465Both modes share the same renderer, dispatched by file extension. The66output is a self-contained HTML page in one-shot mode; in server mode the67same page is served at `/preview` with the theme toggle hidden (the outer68editor page owns it).6970- **`.md`** — YAML frontmatter is stripped (so `marked` doesn't promote it71 to a giant heading via setext rules), the body is rendered with GFM and72 footnotes, code blocks are syntax-highlighted via `highlight.js` and get a73 hover-to-copy button (`copy-button.js`, mermaid blocks skipped), and74 ` ```mermaid ` fenced blocks render to **inline SVG at build time** via75 `beautiful-mermaid` — synchronous, no CDN, no client JS. Diagram colors76 are passed as CSS variables (`var(--bg)`, `var(--fg)`, `var(--accent)`,77 …) matching the page's design tokens, so the light/dark toggle drives the78 diagram live with no re-render. Supports flowchart/state/sequence/class/ER79 /xy with standard Mermaid syntax; `erDiagram` and `classDiagram` make this80 good for schema and architecture diagrams. An invalid diagram renders an81 inline error block (and fails the one-shot build with a non-zero exit).82- **`.yaml` / `.yml`** — rendered as a structured document: top-level keys83 become sections, nested objects nest by heading level, lists of objects84 render as numbered cards, multi-line scalars are treated as markdown.85- **`.feature`** — Gherkin parsed with the official `@cucumber/gherkin`86 parser and rendered semantically: Feature/Rule/Scenario/Background as87 headed sections, steps with color-coded keywords (Given/When/Then),88 data tables and Examples as real tables, doc strings as code blocks,89 tags as chips, and comments (including a `# backed-by:` header) as muted90 notes. Invalid Gherkin falls back to a syntax-highlighted code block so91 the file stays legible and the parse error is easy to spot.92- **`.design`** — a crisp decision note (one Y-statement clause per line)93 with a fixed keyword vocabulary, rendered hierarchically with color-coded94 keywords like Gherkin. Section keywords: `Context:` (forces), `Choose:`95 (decision), `Over:` (alternatives rejected), `Because:` (rationale),96 `So:` (consequences), `Not:` (non-goals). `Key: value` metadata rows97 (`Status`, `Backs`, `Lineage`, `Owner`, `Date`, `Supersedes`) render as a98 muted monospace header, `# ` sets the title, `- ` lines become clause99 bullets, and ` ```mermaid ` blocks render to inline SVG as elsewhere.100101### File layout102103The script is split across files in `scripts/`, inlined at render time via104bun's `import x from "./foo.ext" with { type: "text" }` syntax:105106| File | Role |107|---|---|108| `render-md.ts` | Markdown/YAML/Gherkin/Design → HTML assembly + `serve` subcommand (Bun.serve). Format chosen by extension in `detectFormat`. Mermaid → inline SVG via `beautiful-mermaid`. |109| `styles.css` | Prose design tokens, light/dark themes, code, tables, footnotes, mermaid SVG, copy button, in-preview theme toggle. |110| `theme-toggle.html` | Sun / monitor / moon toggle markup + persistence script (used inside the preview HTML). |111| `editor.html` | Server-mode UI: CodeMirror (via esm.sh import map), mode toggle, theme toggle, autosave. |112| `copy-button.js` | Hover-to-copy button injected onto every `<pre>` code block in the preview (mermaid blocks skipped). |113114Edit any one concern in isolation; no build step. If you change115`styles.css` or `editor.html`, restart the server (`bun run … serve`) —116bun text imports are loaded once at startup.117118Design tokens follow the `prose-typography` skill: 680px measure,11916px/1.6 body, h2 with 2.6em top margin, borders and inline-code120backgrounds derived from the text color at low alpha so the surface121themes coherently in both modes.122123## Conventions124125- Default to one-shot mode unless the user asks to edit, iterate, or126 serve. One-shot is cheaper and disposable.127- One-shot output path defaults to `/tmp/preview.html`. Overwriting is128 fine — this is throwaway preview output.129- Server mode defaults to port `7780`. If that's in use, pass a free130 port as the third argument. Don't try to "park" a server in the131 background without telling the user — they need to know the URL.132- After opening, give a one-line confirmation (what file you rendered,133 where it landed). Don't dump the HTML contents.134- If the user iterates on the source file and asks to "open again", just135 re-run the script — the output path stays stable so the existing136 browser tab refreshes.137138## Why a script instead of inline rendering139140The frontmatter quirk (setext heading promotion when `---` follows141key/value lines) is the kind of thing that bites every time someone142reimplements this inline with `marked`. Bundling the script means we fix143it once. The design tokens, dark mode, theme toggle, footnote handling,144syntax highlighting, mermaid wiring, and table/checkbox styling also145stay consistent across previews.146147## Dependencies148149- `bun` on PATH. The script imports `marked`, `marked-footnote`,150 `highlight.js`, `yaml`, `@cucumber/gherkin`, `@cucumber/messages`, and151 `beautiful-mermaid` — bun auto-installs them on first run. `import … with152 { type: "text" }` for the adjacent CSS/HTML/JS files requires bun ≥ 1.1.153- `open` (macOS) for one-shot mode — substitute `xdg-open` on Linux or154 `start` on Windows.155- Mermaid diagrams render server-side to inline SVG via `beautiful-mermaid`,156 so a doc with diagrams needs **no network at view time** and works157 offline. Server mode still loads CodeMirror from `esm.sh` (with an import158 map pinning shared CodeMirror deps to dedupe state across packages);159 re-renders cache the bundles in the browser.