1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
package usage
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
"text/template"
"unicode"
"github.com/urfave/cli"
md "github.com/russross/blackfriday/v2"
)
var sectionRe = regexp.MustCompile(`(?m:^##)`)
var sectionNameRe = regexp.MustCompile(`(?m:^## [^\n]+)`)
var indentRe = regexp.MustCompile(`(?m:^:[^\n]+)`)
var definitionListRe = regexp.MustCompile(`(?m:^[\t ]+\*\*[^\*]+\*\*[^\n]*\s+:[^\n]+)`)
//var sectionRe = regexp.MustCompile(`^## [^\n]*$`)
type frontmatterData struct {
Data interface{}
Parent string
Children []string
}
// HelpPrinter overwrites cli.HelpPrinter and prints the formatted help to the terminal.
func HelpPrinter(w io.Writer, templ string, data interface{}) {
b := helpPreprocessor(w, templ, data, false)
w.Write(Render(b))
}
func htmlHelpPrinter(w io.Writer, templ string, data interface{}) []byte {
b := helpPreprocessor(w, templ, data, true)
w.Write([]byte(`<html><head><title>step command line documentation</title>`))
w.Write([]byte(`<link href="/style.css" rel="stylesheet" type="text/css">`))
w.Write([]byte(`</head><body><div class="wrapper markdown-body command">`))
html := md.Run(b)
w.Write(html)
w.Write([]byte(`</div></body></html>`))
return html
}
func markdownHelpPrinter(w io.Writer, templ, parent string, data interface{}) {
b := helpPreprocessor(w, templ, data, true)
frontmatter := frontmatterData{
Data: data,
Parent: parent,
}
if app, ok := data.(*cli.App); ok {
for _, cmd := range app.Commands {
frontmatter.Children = append(frontmatter.Children, cmd.Name)
}
}
var frontMatterTemplate = `---
layout: auto-doc
category: reference
title: {{.Data.HelpName}}
menu:
docs:
{{- if .Parent}}
parent: {{.Parent}}
{{- end }}
{{- if .Children }}
children:
{{- range .Children }}
- {{.}}
{{- end }}
{{- end }}
---
`
t, err := template.New("frontmatter").Parse(frontMatterTemplate)
if err != nil {
panic(err)
}
err = t.Execute(w, frontmatter)
if err != nil {
panic(err)
}
w.Write(b)
}
func helpPreprocessor(w io.Writer, templ string, data interface{}, applyRx bool) []byte {
buf := new(bytes.Buffer)
cli.HelpPrinterCustom(buf, templ, data, nil)
//w.Write(buf.Bytes())
// s := string(markdownify(buf.Bytes()))
s := markdownify(buf)
// Move the OPTIONS section to the right place. urfave puts them at the end
// of the file, we want them to be after POSITIONAL ARGUMENTS, DESCRIPTION,
// USAGE, or NAME (in that order, depending on which sections exist).
optLoc := strings.Index(s, "## OPTIONS")
if optLoc != -1 {
optEnd := findSectionEnd("OPTIONS", s)
if optEnd != -1 {
options := s[optLoc:optEnd]
s = s[:optLoc] + s[optEnd:]
if newLoc := findSectionEnd("POSITIONAL ARGUMENTS", s); newLoc != -1 {
s = s[:newLoc] + options + s[newLoc:]
} else if newLoc := findSectionEnd("DESCRIPTION", s); newLoc != -1 {
s = s[:newLoc] + options + s[newLoc:]
} else if newLoc := findSectionEnd("USAGE", s); newLoc != -1 {
s = s[:newLoc] + options + s[newLoc:]
} else if newLoc := findSectionEnd("NAME", s); newLoc != -1 {
s = s[:newLoc] + options + s[newLoc:]
} else {
// Keep it at the end I guess :/.
s += options
}
}
}
if applyRx {
// Keep capitalized only the first letter in arguments names.
s = sectionNameRe.ReplaceAllStringFunc(s, func(s string) string {
return s[0:4] + strings.ToLower(s[4:])
})
// Remove `:` at the start of a line.
s = indentRe.ReplaceAllStringFunc(s, func(s string) string {
return strings.TrimSpace(s[1:])
})
// Convert lines like:
// **Foo**
// : Bar zar ...
// To:
// - **Foo**: Bar zar ...
s = definitionListRe.ReplaceAllStringFunc(s, func(s string) string {
i := strings.Index(s, "\n")
j := strings.Index(s, ":")
return "- " + strings.TrimSpace(s[:i]) + ": " + strings.TrimSpace(s[j+1:])
})
}
return []byte(s)
}
func findSectionEnd(h, s string) int {
start := strings.Index(s, fmt.Sprintf("## %s", h))
if start == -1 {
return start
}
nextSection := sectionRe.FindStringIndex(s[start+2:])
if nextSection == nil {
return len(s)
}
return start + 2 + nextSection[0]
}
// Convert some stuff that we can't easily write in help files because
//
// backticks and raw strings don't mix:
//
// - "<foo>" to "`foo`"
// - "”'" to "```"
func markdownify(r *bytes.Buffer) string {
const escapeByte = byte('\\')
var last byte
var inCode bool
w := new(bytes.Buffer)
for {
b, err := r.ReadByte()
if err != nil {
return w.String()
}
loop:
switch b {
case '<':
if last != escapeByte && !inCode {
w.WriteByte('`')
} else {
w.WriteByte(b)
}
case '>':
if last != escapeByte && !inCode {
w.WriteByte('`')
} else {
w.WriteByte(b)
}
case '\'':
b1, _ := r.ReadByte()
b2, _ := r.ReadByte()
if b1 == b && b2 == b {
w.WriteString("```")
if !inCode {
if n, _, err := r.ReadRune(); err == nil {
if unicode.IsSpace(n) {
w.WriteString("shell")
}
r.UnreadRune()
}
}
inCode = !inCode
} else {
// We can only unread the last one (b2)
w.WriteByte(b)
r.UnreadByte()
b = b1
last = b
goto loop
}
case '*':
if inCode {
if b1, _ := r.ReadByte(); b1 != '*' {
w.WriteByte(b)
w.UnreadByte()
}
} else {
w.WriteByte(b)
}
case escapeByte:
if last == escapeByte {
w.WriteByte(escapeByte)
b = 0
} else if n, _, err := r.ReadRune(); err == nil {
if unicode.IsSpace(n) {
w.WriteByte(escapeByte)
}
r.UnreadRune()
}
case 0: // probably because io.EOF
default:
w.WriteByte(b)
}
last = b
}
}
|