A lightweight, high-performance Go library for rendering Markdown to ANSI-colored terminal output.
- Headings (H1–H6) with distinct styles per level
- Bold, italic, and bold italic
Inline codeand fenced/indented code blocks — green text, language tag in gray- Links — underlined + blue text with gray URL suffix
- Unordered (
-,*) and ordered (1.) lists - GFM tables with UTF-8 box‑drawing borders, alignment (left/center/right), inline formatting inside cells, and automatic text wrapping at a max column width
- Strikethrough text (
~~strikethrough~~) - Task lists (
- [x] done,- [ ] todo) - Thematic break (
---) - Dark and light themes
- ANSI-aware word wrap (
WithWordWrap)
go get github.com/gausszhou/gruff/gruffgo install github.com/gausszhou/gruff@latest
gruff README.md # render a file
gruff render README.md # explicit subcommand
gruff render -w 40 README.md # custom wrap width
gruff render -l README.md # light themepackage main
import (
"fmt"
"log"
"github.com/gausszhou/gruff/gruff"
)
func main() {
md := `# Hello, gruff!
This is **bold**, *italic*, and \`inline code\`.
| Feature | Status |
|---------|--------|
| Tables | ✅ |
| Speed | 🚀 |
`
out, err := gruff.Render(md)
if err != nil {
log.Fatal(err)
}
fmt.Print(out)
}| Function | Description |
|---|---|
WithDark() |
Dark background theme (default) |
WithLight() |
Light background theme |
WithWordWrap(n) |
Wrap output at n columns |
out, err := gruff.Render(source, gruff.WithLight(), gruff.WithWordWrap(80))Benchmarked against testdata/benchmark.md (~2.4 KB) repeated 100× (~240 KB input),
on AMD Ryzen 7 7435H.
| Metric | gruff ¹ | glamour (minimal) ² | glamour (standard) ³ | Improvement (vs minimal / vs standard) |
|---|---|---|---|---|
| Time/op | ~46 ms | ~468 ms | ~2.45 s | ~10× / ~53× |
| Memory/op | ~39 MB | ~86 MB | ~389 MB | ~2.2× / ~10× |
| Allocations/op | ~425,000 | ~6,440,000 | ~32,900,000 | ~15× / ~77× |
¹ gruff: WithDark() (no background), WithWordWrap(120).
² glamour minimal: Chroma = nil, CleanInput, word wrap off, table wrap off, inline table links on.
³ glamour standard: WithStandardStyle("dark"), word wrap at 120 cols.
See docs/why-gruff-faster.md for a detailed analysis of the
performance gap.
Run benchmarks locally:
go test -bench=. -benchmem ./benchmark/Ready-to-run examples are in the examples/ directory:
| Example | Description |
|---|---|
basic |
Render markdown with CLI flags (--light, --wrap) |
table |
Table-specific demo showing alignment and word wrap |
codeblock |
Code block rendering with language tags |
custom-theme |
Custom ANSI color and style customization |
api |
Render, RenderBytes, and WithWordWrap usage |
compare-benchmark |
Side-by-side benchmark markdown rendered with gruff vs glamour |
compare-glamour |
Side-by-side glamour standard vs minimal |
compare-theme |
Side-by-side gruff dark vs light theme |
compare-simple |
Glamour minimal vs standard without viewport/bubbletea |
viewport-gruff |
Gruff output in a bubbletea viewport with scrollbar |
viewport-glamour |
Glamour output in a bubbletea viewport with scrollbar |
go run examples/basic/main.go
go run examples/table/main.go
go run examples/codeblock/main.go
go run examples/custom-theme/main.go
go run examples/api/main.go
go run examples/compare-benchmark/main.go
go run examples/compare-glamour/main.go
go run examples/compare-theme/main.go
go run examples/compare-simple/main.go
go run examples/viewport-gruff/main.go
go run examples/viewport-glamour/main.goUse the exported Theme, Style, and Color types to customize colors and styles:
import "github.com/gausszhou/gruff"
customTheme := func() gruff.Option {
return func(o *gruff.Options) {
o.Theme.H1 = gruff.Style{Fg: gruff.Color(196), Bold: true} // red
o.Theme.Strong = gruff.Style{Bold: true, Fg: gruff.Color(51)} // cyan
o.Theme.Code = gruff.Style{Fg: gruff.Color("#50865a")} // green
o.Theme.Link = gruff.Style{Underline: true, Fg: gruff.Color("#5c9cf5")}
}
}
out, _ := gruff.Render(md, customTheme())Parsing is handled by goldmark. The AST is walked by a recursive type switch in renderer.go that emits SGR ANSI codes directly — no intermediate DOM, no CSS, no HTML. Each inline style uses specific undo codes (e.g. \x1b[22m for no-bold, \x1b[39m for default foreground) instead of \x1b[0m, so nested formatting is preserved correctly.
Table rendering uses a two-pass approach: collect all cell content and calculate column widths, then emit UTF-8 box‑drawing borders and padded cell content. Columns cap at a maximum width with automatic word wrapping.
Code blocks are rendered line-by-line with language tags shown in gray, content in green, and each line padded to the document width for a clean full-width appearance.
Runtime: github.com/yuin/goldmark, github.com/clipperhouse/displaywidth, github.com/mattn/go-runewidth
Examples only: charm.land/bubbles/v2, charm.land/bubbletea/v2, charm.land/lipgloss/v2, charm.land/glamour/v2 (not included in library builds)
MIT