- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .memory-bank | ||
| examples | ||
| internal | ||
| mermaid | ||
| testdata | ||
| .gitignore | ||
| alert.go | ||
| alert_test.go | ||
| badge.go | ||
| builder.go | ||
| errors.go | ||
| example_bar_test.go | ||
| go.mod | ||
| go.sum | ||
| index.go | ||
| index_test.go | ||
| list_builder.go | ||
| lists.go | ||
| lists_test.go | ||
| markdown.go | ||
| markdown_test.go | ||
| nodes.go | ||
| README.md | ||
| renderer.go | ||
| SECURITY.md | ||
| syntax_sugar.go | ||
| table_builder.go | ||
| table_builder_test.go | ||
| table_html.go | ||
| table_html_test.go | ||
| tables.go | ||
| tables_test.go | ||
markdown
markdown is a lightweight builder for generating Markdown programmatically using the goldmark AST. It mirrors the API provided by github.com/nao1215/markdown while dropping external dependencies (such as tablewriter) and relying entirely on goldmark nodes for construction and rendering.
Features
- Chainable builder API for headings, lists, blockquotes, tables, callouts, badges, links, and more
- Nested lists up to five levels deep, via a declarative item tree or a closure builder
- Table of Contents generation with configurable depth
- Table rendering with per-column alignment and auto-formatting helpers, via a struct or a closure builder
- HTML table snippets converted to Markdown tables
- Custom table handling without
tablewriter - Goldmark-backed internal representation ensures consistent Markdown output across platforms
- Simple syntax sugar helpers for inline formatting
- Range of mermaid charts, graphics and diagrams including sequence, user journey, git graph, state and class diagrams, flowcharts, pie charts and Gnatt charts (review
mermaidfolder for full listing)
Installation
go get rtlabs.tech/markdown
Quick Start
package main
import (
"fmt"
"os"
"rtlabs.tech/markdown"
)
func main() {
md := markdown.NewMarkdown(os.Stdout)
md.H1("Guide to markdown").
PlainText("Markdown built through goldmark AST.").
Table(markdown.TableSet{
Header: []string{"Feature", "Description"},
Rows: [][]string{
{"TOC", "Generate nested table of contents"},
{"Tables", "Alignment-aware rendering without tablewriter"},
},
}).
Build()
}
Output:
# Guide to markdown
Markdown built through goldmark AST.
| Feature | Description |
| ------- | --------------------------------------------- |
| TOC | Generate nested table of contents |
| Tables | Alignment-aware rendering without tablewriter |
Building Documents
Every method on *Markdown returns the same builder, enabling fluent composition. When you’re done, call Build() to write the rendered Markdown to the provided io.Writer.
md := markdown.NewMarkdown(os.Stdout)
md.H1("Release Notes").
H2("v1.0.0").
BulletList("Initial release", "Markdown builder", "Table support").
LF().
Important("Remember to pin dependencies").
Build()
Adding a Table of Contents
TableOfContents consumes the recorded heading metadata and writes a Markdown TOC up to a specified depth.
md := markdown.NewMarkdown(os.Stdout)
md.H1("Project").
H2("Overview").
H2("Usage").
TableOfContents(markdown.TableOfContentsDepthH2).
Build()
The generated TOC uses bullet indentation to reflect heading levels.
Nested Lists
BulletList and OrderedList remain the shortcut for flat lists. For hierarchies there are two interchangeable APIs, both backed by the same implementation and both limited to markdown.MaxListDepth (5) levels.
Declarative item tree
ListItem values describe the tree; Item, BulletItem, OrderedItem, and TaskItem are constructor helpers.
md.NestedBulletList(
markdown.Item("Fruit",
markdown.Item("Apple"),
markdown.OrderedItem("Citrus",
markdown.Item("Lemon"),
markdown.Item("Lime"),
),
),
markdown.Item("Vegetables"),
)
Output:
- Fruit
- Apple
- Citrus
1. Lemon
2. Lime
- Vegetables
An item's Style selects the marker used by its children, so styles can be mixed freely. ListStyleInherit (the zero value) reuses the parent's style. NestedList(ListSet{...}) is the canonical entry point when you need to set the top-level style or an ordered list's Start value directly.
Closure builder
ListBuilder layers a fluent, closure-driven API over the same tree. Sub inherits the parent style while Bullets, Numbers, and Tasks select one explicitly.
md.OrderedListFunc(func(l *markdown.ListBuilder) {
l.Item("Install").Sub(func(l *markdown.ListBuilder) {
l.Item("Download the archive")
l.Item("Verify the checksum")
})
l.Item("Configure").Bullets(func(l *markdown.ListBuilder) {
l.Item("Edit the config file")
})
})
Output:
1. Install
1. Download the archive
2. Verify the checksum
2. Configure
- Edit the config file
Child lists are indented by the width of the parent marker (three spaces beneath 1. , two beneath - ) so the output stays valid CommonMark even past item 9.
BulletListFunc, OrderedListFunc, CheckBoxFunc, and ListFunc are the available entry points.
Nested task lists
md.NestedCheckBox(
markdown.TaskItem("Release", false,
markdown.TaskItem("Tag version", true),
markdown.TaskItem("Publish notes", false),
),
)
- [ ] Release
- [x] Tag version
- [ ] Publish notes
Depth limit
Nesting is capped at five levels. Rather than discarding the whole list, the builder renders the levels within the limit, truncates anything deeper, and records ErrMaxListDepthExceeded so callers can react programmatically:
md.NestedBulletList(deeplyNestedItems)
if err := md.Error(); errors.Is(err, markdown.ErrMaxListDepthExceeded) {
log.Printf("list was truncated to %d levels", markdown.MaxListDepth)
}
Similarly, calling Sub/Bullets/Numbers/Tasks before any item exists at that level is a no-op that records ErrListItemMissing.
Working with Tables
Tables are defined through TableSet. The renderer automatically pads columns to fit the widest cell and emits separators honoring column alignment.
md.Table(markdown.TableSet{
Header: []string{"Left", "Center", "Right"},
Rows: [][]string{
{"L", "C", "R"},
},
Alignment: []markdown.TableAlignment{
markdown.AlignLeft,
markdown.AlignCenter,
markdown.AlignRight,
},
})
Output:
| Left | Center | Right |
| :--- | :----: | ----: |
| L | C | R |
Custom Table Helpers
CustomTable applies optional formatting on top of standard rendering. Currently, it supports:
AutoFormatHeaders: Title-cases header cells by splitting on whitespace
md.CustomTable(markdown.TableSet{
Header: []string{"first name", "status"},
Rows: [][]string{{"Alice", "active"}},
}, markdown.TableOptions{AutoFormatHeaders: true})
Building Tables with a Closure
TableFunc layers a closure builder over TableSet, which suits tables whose
rows are computed. CustomTableFunc accepts the same TableOptions as
CustomTable.
md.TableFunc(func(t *markdown.TableBuilder) {
t.Column("Service").
ColumnWith("Replicas", markdown.AlignRight).
ColumnWith("Status", markdown.AlignCenter)
t.Row("api", "3", "healthy")
t.RowFunc(func(r *markdown.TableRowBuilder) {
r.Cell("worker").Cellf("%d", replicas).Cell("degraded")
})
})
| Service | Replicas | Status |
| ------- | -------: | :------: |
| api | 3 | healthy |
| worker | 5 | degraded |
Use Headers and Align to declare several columns at once. A row whose width
does not match the columns records ErrMismatchColumn, and a row added before
any column records ErrTableHeaderMissing; both surface through Error().
Converting HTML Tables
HTMLTable converts an HTML snippet containing a table. The input does not need
to be a whole document — just the fragment holding the table — and malformed or
unclosed markup is tolerated.
md.HTMLTable(`<table>
<thead><tr><th>Name</th><th align="right">Age</th></tr></thead>
<tbody><tr><td><strong>Alice</strong></td><td>24</td></tr></tbody>
</table>`)
| Name | Age |
| ----- | --: |
| Alice | 24 |
ParseHTMLTable returns the TableSet instead of appending it, so the data can
be inspected or amended before rendering:
set, err := markdown.ParseHTMLTable(snippet)
Conversion behaviour:
- The header comes from
<thead>, else the first row containing<th>, else the first row. Override withHTMLTableOptions.HeaderusingHTMLHeaderFirstRoworHTMLHeaderNone. alignattributes and simpletext-alignstyles map onto column alignment.- Only the first table is converted; a table nested inside a cell is skipped and
reported with
ErrNestedHTMLTable. - Whitespace is collapsed unless
PreserveWhitespaceis set. Newlines always become spaces, since a literal newline would end the row.
Two aspects are necessarily lossy, because Markdown tables cannot express them:
- Inline markup is stripped to plain text.
<strong>,<a>and<code>are dropped,<br>becomes a space, and|is escaped to\|so cell content cannot break out of its column. colspanandrowspanare flattened. The cell's text is repeated across every spanned column and row, andErrTableSpanFlattenedis recorded. The table still renders, so checkerrors.Is(md.Error(), markdown.ErrTableSpanFlattened)if merged cells matter to you.
A snippet with no table at all records ErrNoHTMLTableFound and appends nothing.
Inline Formatting Helpers
Use the standalone helpers for inline Markdown strings:
markdown.Bold("text") // **text**
markdown.Italic("text") // *text*
markdown.Link("Docs", "https://example.com")
markdown.Image("Logo", "https://example.com/logo.png")
markdown.Highlight("Note") // ==Note==
Callouts and Badges
The builder supports GitHub-style callouts and shield badges:
md.Note("Heads up!")
md.Tip("Try the new API.")
md.BlueBadge("stable")
Each callout renders a blockquote with the appropriate label (e.g., [!NOTE]).
Rendering Programmatically Generated Data
The powered example below demonstrates building a weekly price table from structs:
func ExampleMarkdown_Table_bars() {
bars := []Bar{ /* ... seven days ... */ }
rows := make([][]string, len(bars))
for i, bar := range bars {
rows[i] = []string{
bar.Timestamp.Format("2006-01-02"),
fmt.Sprintf("%.2f", bar.Open),
fmt.Sprintf("%.2f", bar.High),
fmt.Sprintf("%.2f", bar.Low),
fmt.Sprintf("%.2f", bar.Close),
fmt.Sprintf("%d", bar.Volume),
fmt.Sprintf("%d", bar.TradeCount),
fmt.Sprintf("%.2f", bar.VWAP),
}
}
md := markdown.NewMarkdown(os.Stdout)
md.H2("Daily Bars")
md.Table(markdown.TableSet{
Header: []string{"Day", "Open", "High", "Low", "Close", "Volume", "Trades", "VWAP"},
Rows: rows,
})
md.Build()
}
Creating an index for a directory full of markdown files
The markdown package can create an index for Markdown files within the specified directory. This feature was added to generate indexes for large scale markdown document generation.
For example, consider the following directory structure:
testdata
├── abc
│ ├── dummy.txt
│ ├── jkl
│ │ └── text.md
│ └── test.md
├── def
│ ├── test.md
│ └── test2.md
├── expected
│ └── index.md
├── ghi
└── test.md
In the following implementation, it creates an index markdown file containing links to all markdown files located within the testdata directory.
if err := GenerateIndex(
"testdata", // target directory that contains markdown files
WithTitle("Test Title"), // title of index markdown
WithDescription([]string{"Test Description", "Next Description"}), // description of index markdown
); err != nil {
panic(err)
}
Error Handling
Most builder methods return the builder and only record errors internally. Retrieve the combined error from Error() or defer the check to Build():
md.Table(markdown.TableSet{Header: []string{"A"}, Rows: [][]string{{"x", "y"}}})
if err := md.Build(); err != nil {
log.Fatalf("build failed: %v", err)
}
Testing
Run project tests with:
go test ./...
Copyright and License
Code and Documentation Copyright © 1994-2023, Stephen Kapp and Reaper Technologies Limited. All Rights Reserved.