No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-30 03:20:21 +01:00
.memory-bank cline-mem: record table builder and HTML import patterns 2026-07-30 03:15:00 +01:00
examples sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
internal index generation, better errors, atomic save 2026-07-29 13:05:45 +01:00
mermaid mermaid diagrams and charts 2026-07-29 13:06:43 +01:00
testdata index generation, better errors, atomic save 2026-07-29 13:05:45 +01:00
.gitignore initial commit 2026-07-27 04:51:44 +01:00
alert.go implementation of builder 2026-07-29 04:45:07 +01:00
alert_test.go index generation, better errors, atomic save 2026-07-29 13:05:45 +01:00
badge.go implementation of builder 2026-07-29 04:45:07 +01:00
builder.go tweaks to structure 2026-07-30 02:42:49 +01:00
errors.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
example_bar_test.go implementation of builder 2026-07-29 04:45:07 +01:00
go.mod sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
go.sum sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
index.go index generation, better errors, atomic save 2026-07-29 13:05:45 +01:00
index_test.go index generation, better errors, atomic save 2026-07-29 13:05:45 +01:00
list_builder.go implementation of builder 2026-07-29 04:45:07 +01:00
lists.go implementation of builder 2026-07-29 04:45:07 +01:00
lists_test.go implementation of builder 2026-07-29 04:45:07 +01:00
markdown.go tweaks to structure 2026-07-30 02:42:49 +01:00
markdown_test.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
nodes.go implementation of builder 2026-07-29 04:45:07 +01:00
README.md sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
renderer.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
SECURITY.md initial commit 2026-07-27 04:51:44 +01:00
syntax_sugar.go implementation of builder 2026-07-29 04:45:07 +01:00
table_builder.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
table_builder_test.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
table_html.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
table_html_test.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
tables.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00
tables_test.go sync table api to approach for lists, implement HTML2MD table converter 2026-07-30 03:20:21 +01:00

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 mermaid folder 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 youre 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 with HTMLTableOptions.Header using HTMLHeaderFirstRow or HTMLHeaderNone.
  • align attributes and simple text-align styles 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 PreserveWhitespace is 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.
  • colspan and rowspan are flattened. The cell's text is repeated across every spanned column and row, and ErrTableSpanFlattened is recorded. The table still renders, so check errors.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 ./...

Code and Documentation Copyright © 1994-2023, Stephen Kapp and Reaper Technologies Limited. All Rights Reserved.