Skip to content

Commit

Permalink
Merge pull request #1722 from tarampampam/feat-tabular-markdown
Browse files Browse the repository at this point in the history
Feat: Added tabular markdown writer
  • Loading branch information
dearchap committed May 1, 2023
2 parents 62d51b1 + 9640f32 commit acbbbf2
Show file tree
Hide file tree
Showing 8 changed files with 936 additions and 0 deletions.
315 changes: 315 additions & 0 deletions docs.go
Expand Up @@ -7,13 +7,94 @@ import (
"bytes"
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
"text/template"
"unicode/utf8"

"github.com/cpuguy83/go-md2man/v2/md2man"
)

// ToTabularMarkdown creates a tabular markdown documentation for the `*App`.
// The function errors if either parsing or writing of the string fails.
func (a *App) ToTabularMarkdown(appPath string) (string, error) {
if appPath == "" {
appPath = "app"
}

const name = "cli"

t, err := template.New(name).Funcs(template.FuncMap{
"join": strings.Join,
}).Parse(MarkdownTabularDocTemplate)
if err != nil {
return "", err
}

var (
w bytes.Buffer
tt tabularTemplate
)

if err = t.ExecuteTemplate(&w, name, cliTabularAppTemplate{
AppPath: appPath,
Name: a.Name,
Description: tt.PrepareMultilineString(a.Description),
Usage: tt.PrepareMultilineString(a.Usage),
UsageText: strings.FieldsFunc(a.UsageText, func(r rune) bool { return r == '\n' }),
ArgsUsage: tt.PrepareMultilineString(a.ArgsUsage),
GlobalFlags: tt.PrepareFlags(a.VisibleFlags()),
Commands: tt.PrepareCommands(a.VisibleCommands(), appPath, "", 0),
}); err != nil {
return "", err
}

return tt.Prettify(w.String()), nil
}

// ToTabularToFileBetweenTags creates a tabular markdown documentation for the `*App` and updates the file between
// the tags in the file. The function errors if either parsing or writing of the string fails.
func (a *App) ToTabularToFileBetweenTags(appPath, filePath string, startEndTags ...string) error {
var start, end = "<!--GENERATED:CLI_DOCS-->", "<!--/GENERATED:CLI_DOCS-->" // default tags

if len(startEndTags) == 2 {
start, end = startEndTags[0], startEndTags[1]
}

// read original file content
content, err := os.ReadFile(filePath)
if err != nil {
return err
}

// generate markdown
md, err := a.ToTabularMarkdown(appPath)
if err != nil {
return err
}

// prepare regexp to replace content between start and end tags
re, err := regexp.Compile("(?s)" + regexp.QuoteMeta(start) + "(.*?)" + regexp.QuoteMeta(end))
if err != nil {
return err
}

const comment = "<!-- Documentation inside this block generated by github.com/urfave/cli; DO NOT EDIT -->"

// replace content between start and end tags
updated := re.ReplaceAll(content, []byte(strings.Join([]string{start, comment, md, end}, "\n")))

// write updated content to file
if err = os.WriteFile(filePath, updated, 0664); err != nil {
return err
}

return nil
}

// ToMarkdown creates a markdown string for the `*App`
// The function errors if either parsing or writing of the string fails.
func (a *App) ToMarkdown() (string, error) {
Expand Down Expand Up @@ -196,3 +277,237 @@ func prepareUsage(command *Command, usageText string) string {

return usage
}

type (
cliTabularAppTemplate struct {
AppPath string
Name string
Usage string
ArgsUsage string
UsageText []string
Description string
GlobalFlags []cliTabularFlagTemplate
Commands []cliTabularCommandTemplate
}

cliTabularCommandTemplate struct {
AppPath string
Name string
Aliases []string
Usage string
ArgsUsage string
UsageText []string
Description string
Category string
Flags []cliTabularFlagTemplate
SubCommands []cliTabularCommandTemplate
Level uint
}

cliTabularFlagTemplate struct {
Name string
Aliases []string
Usage string
TakesValue bool
Default string
EnvVars []string
}
)

// tabularTemplate is a struct for the tabular template preparation.
type tabularTemplate struct{}

// PrepareCommands converts CLI commands into a structs for the rendering.
func (tt tabularTemplate) PrepareCommands(commands []*Command, appPath, parentCommandName string, level uint) []cliTabularCommandTemplate {
var result = make([]cliTabularCommandTemplate, 0, len(commands))

for _, cmd := range commands {
var command = cliTabularCommandTemplate{
AppPath: appPath,
Name: strings.TrimSpace(strings.Join([]string{parentCommandName, cmd.Name}, " ")),
Aliases: cmd.Aliases,
Usage: tt.PrepareMultilineString(cmd.Usage),
UsageText: strings.FieldsFunc(cmd.UsageText, func(r rune) bool { return r == '\n' }),
ArgsUsage: tt.PrepareMultilineString(cmd.ArgsUsage),
Description: tt.PrepareMultilineString(cmd.Description),
Category: cmd.Category,
Flags: tt.PrepareFlags(cmd.VisibleFlags()),
SubCommands: tt.PrepareCommands( // note: recursive call
cmd.Commands,
appPath,
strings.Join([]string{parentCommandName, cmd.Name}, " "),
level+1,
),
Level: level,
}

result = append(result, command)
}

return result
}

// PrepareFlags converts CLI flags into a structs for the rendering.
func (tt tabularTemplate) PrepareFlags(flags []Flag) []cliTabularFlagTemplate {
var result = make([]cliTabularFlagTemplate, 0, len(flags))

for _, appFlag := range flags {
flag, ok := appFlag.(DocGenerationFlag)
if !ok {
continue
}

var f = cliTabularFlagTemplate{
Usage: tt.PrepareMultilineString(flag.GetUsage()),
EnvVars: flag.GetEnvVars(),
TakesValue: flag.TakesValue(),
Default: flag.GetValue(),
}

if boolFlag, isBool := appFlag.(*BoolFlag); isBool {
f.Default = strconv.FormatBool(boolFlag.Value)
}

for i, name := range flag.Names() {
name = strings.TrimSpace(name)

if i == 0 {
f.Name = "--" + name

continue
}

if len(name) > 1 {
name = "--" + name
} else {
name = "-" + name
}

f.Aliases = append(f.Aliases, name)
}

result = append(result, f)
}

return result
}

// PrepareMultilineString prepares a string (removes line breaks).
func (tabularTemplate) PrepareMultilineString(s string) string {
return strings.TrimRight(
strings.TrimSpace(
strings.ReplaceAll(s, "\n", " "),
),
".\r\n\t",
)
}

func (tabularTemplate) Prettify(s string) string {
var max = func(x, y int) int {
if x > y {
return x
}
return y
}

var b strings.Builder

// search for tables
for _, rawTable := range regexp.MustCompile(`(?m)^(\|[^\n]+\|\r?\n)((?:\|:?-+:?)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$`).FindAllString(s, -1) {
var lines = strings.FieldsFunc(rawTable, func(r rune) bool { return r == '\n' })

if len(lines) < 3 { // header, separator, body
continue
}

// parse table into the matrix
var matrix = make([][]string, 0, len(lines))
for _, line := range lines {
items := strings.FieldsFunc(strings.Trim(line, "| "), func(r rune) bool { return r == '|' })

for i := range items {
items[i] = strings.TrimSpace(items[i]) // trim spaces in cells
}

matrix = append(matrix, items)
}

// determine centered columns
var centered = make([]bool, 0, len(matrix[1]))
for _, cell := range matrix[1] {
centered = append(centered, strings.HasPrefix(cell, ":") && strings.HasSuffix(cell, ":"))
}

// calculate max lengths
var lengths = make([]int, len(matrix[0]))
for n, row := range matrix {
for i, cell := range row {
if n == 1 {
continue // skip separator
}

if l := utf8.RuneCountInString(cell); l > lengths[i] {
lengths[i] = l
}
}
}

// format cells
for i, row := range matrix {
for j, cell := range row {
if i == 1 { // is separator
if centered[j] {
b.Reset()
b.WriteRune(':')
b.WriteString(strings.Repeat("-", max(0, lengths[j])))
b.WriteRune(':')

row[j] = b.String()
} else {
row[j] = strings.Repeat("-", max(0, lengths[j]+2))
}

continue
}

var (
cellWidth = utf8.RuneCountInString(cell)
padLeft, padRight = 1, max(1, lengths[j]-cellWidth+1) // align to the left
)

if centered[j] { // is centered
padLeft = max(1, (lengths[j]-cellWidth)/2)
padRight = max(1, lengths[j]-cellWidth-(padLeft-1))
}

b.Reset()
b.WriteString(strings.Repeat(" ", padLeft))

if padLeft+cellWidth+padRight <= lengths[j]+1 {
b.WriteRune(' ') // add an extra space if the cell is not full
}

b.WriteString(cell)
b.WriteString(strings.Repeat(" ", padRight))

row[j] = b.String()
}
}

b.Reset()

for _, row := range matrix { // build new table
b.WriteRune('|')
b.WriteString(strings.Join(row, "|"))
b.WriteRune('|')
b.WriteRune('\n')
}

s = strings.Replace(s, rawTable, b.String(), 1)
}

s = regexp.MustCompile(`\n{2,}`).ReplaceAllString(s, "\n\n") // normalize newlines
s = strings.Trim(s, " \n") // trim spaces and newlines

return s + "\n" // add an extra newline
}

0 comments on commit acbbbf2

Please sign in to comment.