WaveDrom Timing Diagrams in Pelican with Claude Code

WaveDrom is a JavaScript library that renders digital timing diagrams from a simple JSON-based format called WaveJSON. It's widely used in hardware documentation — you describe signal transitions in a compact string notation, and WaveDrom draws the waveform. This post covers two tools I've built around it: a Pelican plugin that renders diagrams at build time, and a Claude Code skill that generates WaveJSON from plain English.

The Pelican plugin

The plugin is at github.com/morganp/pelican-wavedrom, cloned to ~/Code/pelican-wavedrom — a sibling directory to the blog repo. It intercepts fenced ```wavedrom ``` code blocks before Pelican's standard Markdown processing, renders each one to an SVG using wavedrom-cli, caches the result by content hash in content/images/wavedrom/, and replaces the block with a standard image reference. Pelican then copies the SVG to output/images/wavedrom/ as a static asset.

SVGs are cached across builds — only diagrams whose source has changed are re-rendered.

Installation

The plugin requires wavedrom-cli globally via npm:

npm install -g wavedrom-cli

Then clone the plugin alongside your blog repo and install it into the Pelican virtualenv in editable mode:

git clone https://github.com/morganp/pelican-wavedrom ../pelican-wavedrom
source venv/bin/activate
pip install -e ../pelican-wavedrom --config-settings editable_mode=compat

The editable_mode=compat flag is required. Without it, modern setuptools editable installs use a path-hook mechanism that prevents Pelican's namespace plugin auto-discovery from finding the plugin.

Pelican 4.5+ auto-discovers namespace plugins — no changes to pelicanconf.py are needed.

Optional config

If wavedrom-cli is not on your PATH during the build (e.g. in a CI environment), set its full path in pelicanconf.py:

WAVEDROM_CLI = '/opt/homebrew/bin/wavedrom-cli'

Using it in a post

Write a fenced code block with the language set to wavedrom:

```wavedrom
{ "signal": [
  { "name": "CLK",     "wave": "p.....|..." },
  { "name": "Data",    "wave": "x.345x|=.x", "data": ["head", "body", "tail", "data"] },
  { "name": "Request", "wave": "0.1..0|1.0" }
]}
```

At build time (make html or make github) this becomes an SVG embedded in the page:

WaveDrom timing diagram

If wavedrom-cli is not found or rendering fails, the block falls back to a fenced json block — the post still builds, you just see the raw JSON instead of a diagram.

The Claude Code skill

The skill lives at ~/.claude/skills/wavedrom/SKILL.md. Claude Code auto-discovers skills from ~/.claude/skills/ and loads them on demand.

The skill triggers automatically when you describe anything related to timing diagrams, waveforms, or digital protocols — SPI, I2C, UART, AXI handshakes, clock enables, request/acknowledge patterns. You describe the signals in plain English; Claude generates the WaveJSON.

The skill includes:

  • The full WaveJSON wave-character reference (p, n, 0, 1, x, z, ., =, 29, |)
  • Signal properties (phase, period, node)
  • Top-level properties (edge, config, head, foot)
  • Group and spacer syntax
  • Edge annotation syntax for timing arrows between signals
  • Common patterns: SPI transactions, request/acknowledge handshakes, clock-with-enable, grouped signals

Example workflow

Describe the protocol:

"Draw an I2C start condition followed by a 7-bit address byte with ACK"

Claude generates the WaveJSON and explains the timing. Because the skill outputs a wavedrom fenced block (not just JSON), you can paste it directly into a blog post and the Pelican plugin renders it automatically.

WaveJSON quick reference

The wave string for each signal is a sequence of characters:

Char Meaning
p / n Positive / negative clock (with tick mark)
0 / 1 Logic low / high
x Unknown / undefined
z High impedance
. Continue previous state
= Multi-bit data (label from data array)
29 Coloured data states
\| Gap / break in time axis

A minimal diagram:

WaveDrom timing diagram

An SPI transaction with chip select:

WaveDrom timing diagram

A request/acknowledge handshake with an edge annotation showing propagation delay:

WaveDrom timing diagram

Project structure

~/Code/
├── morganp.github.io/          # blog source (main branch)
   └── content/images/wavedrom/  # SVG cache (persists across make clean)
└── pelican-wavedrom/           # github.com/morganp/pelican-wavedrom
    └── pelican/plugins/wavedrom_generator/
        ├── __init__.py         # plugin entry point, signal registration
        └── preprocessor.py    # Markdown extension + preprocessor

~/.claude/skills/
└── wavedrom/
    └── SKILL.md                # Claude Code skill definition

The combination is useful for hardware documentation: describe a protocol in English, get WaveJSON from Claude, drop the block into a post, and the plugin renders it at build time. No manual JSON editing, no copy-pasting between browser tabs.

The same skill format is used by the Verilog lint skill — describe a module, get generated RTL, and the lint loop validates it automatically.

social