mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-29 23:30:04 +01:00
d5c180e680
It is better developing vmctl tool in VictoriaMetrics repository, so it could be released together with the rest of vmutils tools such as vmalert, vmagent, vmbackup, vmrestore and vmauth.
346 lines
8.4 KiB
Go
346 lines
8.4 KiB
Go
package md2man
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/russross/blackfriday/v2"
|
|
)
|
|
|
|
// roffRenderer implements the blackfriday.Renderer interface for creating
|
|
// roff format (manpages) from markdown text
|
|
type roffRenderer struct {
|
|
extensions blackfriday.Extensions
|
|
listCounters []int
|
|
firstHeader bool
|
|
defineTerm bool
|
|
listDepth int
|
|
}
|
|
|
|
const (
|
|
titleHeader = ".TH "
|
|
topLevelHeader = "\n\n.SH "
|
|
secondLevelHdr = "\n.SH "
|
|
otherHeader = "\n.SS "
|
|
crTag = "\n"
|
|
emphTag = "\\fI"
|
|
emphCloseTag = "\\fP"
|
|
strongTag = "\\fB"
|
|
strongCloseTag = "\\fP"
|
|
breakTag = "\n.br\n"
|
|
paraTag = "\n.PP\n"
|
|
hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n"
|
|
linkTag = "\n\\[la]"
|
|
linkCloseTag = "\\[ra]"
|
|
codespanTag = "\\fB\\fC"
|
|
codespanCloseTag = "\\fR"
|
|
codeTag = "\n.PP\n.RS\n\n.nf\n"
|
|
codeCloseTag = "\n.fi\n.RE\n"
|
|
quoteTag = "\n.PP\n.RS\n"
|
|
quoteCloseTag = "\n.RE\n"
|
|
listTag = "\n.RS\n"
|
|
listCloseTag = "\n.RE\n"
|
|
arglistTag = "\n.TP\n"
|
|
tableStart = "\n.TS\nallbox;\n"
|
|
tableEnd = ".TE\n"
|
|
tableCellStart = "T{\n"
|
|
tableCellEnd = "\nT}\n"
|
|
)
|
|
|
|
// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents
|
|
// from markdown
|
|
func NewRoffRenderer() *roffRenderer { // nolint: golint
|
|
var extensions blackfriday.Extensions
|
|
|
|
extensions |= blackfriday.NoIntraEmphasis
|
|
extensions |= blackfriday.Tables
|
|
extensions |= blackfriday.FencedCode
|
|
extensions |= blackfriday.SpaceHeadings
|
|
extensions |= blackfriday.Footnotes
|
|
extensions |= blackfriday.Titleblock
|
|
extensions |= blackfriday.DefinitionLists
|
|
return &roffRenderer{
|
|
extensions: extensions,
|
|
}
|
|
}
|
|
|
|
// GetExtensions returns the list of extensions used by this renderer implementation
|
|
func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
|
|
return r.extensions
|
|
}
|
|
|
|
// RenderHeader handles outputting the header at document start
|
|
func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
|
|
// disable hyphenation
|
|
out(w, ".nh\n")
|
|
}
|
|
|
|
// RenderFooter handles outputting the footer at the document end; the roff
|
|
// renderer has no footer information
|
|
func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
|
|
}
|
|
|
|
// RenderNode is called for each node in a markdown document; based on the node
|
|
// type the equivalent roff output is sent to the writer
|
|
func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
|
|
|
var walkAction = blackfriday.GoToNext
|
|
|
|
switch node.Type {
|
|
case blackfriday.Text:
|
|
r.handleText(w, node, entering)
|
|
case blackfriday.Softbreak:
|
|
out(w, crTag)
|
|
case blackfriday.Hardbreak:
|
|
out(w, breakTag)
|
|
case blackfriday.Emph:
|
|
if entering {
|
|
out(w, emphTag)
|
|
} else {
|
|
out(w, emphCloseTag)
|
|
}
|
|
case blackfriday.Strong:
|
|
if entering {
|
|
out(w, strongTag)
|
|
} else {
|
|
out(w, strongCloseTag)
|
|
}
|
|
case blackfriday.Link:
|
|
if !entering {
|
|
out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag)
|
|
}
|
|
case blackfriday.Image:
|
|
// ignore images
|
|
walkAction = blackfriday.SkipChildren
|
|
case blackfriday.Code:
|
|
out(w, codespanTag)
|
|
escapeSpecialChars(w, node.Literal)
|
|
out(w, codespanCloseTag)
|
|
case blackfriday.Document:
|
|
break
|
|
case blackfriday.Paragraph:
|
|
// roff .PP markers break lists
|
|
if r.listDepth > 0 {
|
|
return blackfriday.GoToNext
|
|
}
|
|
if entering {
|
|
out(w, paraTag)
|
|
} else {
|
|
out(w, crTag)
|
|
}
|
|
case blackfriday.BlockQuote:
|
|
if entering {
|
|
out(w, quoteTag)
|
|
} else {
|
|
out(w, quoteCloseTag)
|
|
}
|
|
case blackfriday.Heading:
|
|
r.handleHeading(w, node, entering)
|
|
case blackfriday.HorizontalRule:
|
|
out(w, hruleTag)
|
|
case blackfriday.List:
|
|
r.handleList(w, node, entering)
|
|
case blackfriday.Item:
|
|
r.handleItem(w, node, entering)
|
|
case blackfriday.CodeBlock:
|
|
out(w, codeTag)
|
|
escapeSpecialChars(w, node.Literal)
|
|
out(w, codeCloseTag)
|
|
case blackfriday.Table:
|
|
r.handleTable(w, node, entering)
|
|
case blackfriday.TableCell:
|
|
r.handleTableCell(w, node, entering)
|
|
case blackfriday.TableHead:
|
|
case blackfriday.TableBody:
|
|
case blackfriday.TableRow:
|
|
// no action as cell entries do all the nroff formatting
|
|
return blackfriday.GoToNext
|
|
default:
|
|
fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
|
|
}
|
|
return walkAction
|
|
}
|
|
|
|
func (r *roffRenderer) handleText(w io.Writer, node *blackfriday.Node, entering bool) {
|
|
var (
|
|
start, end string
|
|
)
|
|
// handle special roff table cell text encapsulation
|
|
if node.Parent.Type == blackfriday.TableCell {
|
|
if len(node.Literal) > 30 {
|
|
start = tableCellStart
|
|
end = tableCellEnd
|
|
} else {
|
|
// end rows that aren't terminated by "tableCellEnd" with a cr if end of row
|
|
if node.Parent.Next == nil && !node.Parent.IsHeader {
|
|
end = crTag
|
|
}
|
|
}
|
|
}
|
|
out(w, start)
|
|
escapeSpecialChars(w, node.Literal)
|
|
out(w, end)
|
|
}
|
|
|
|
func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) {
|
|
if entering {
|
|
switch node.Level {
|
|
case 1:
|
|
if !r.firstHeader {
|
|
out(w, titleHeader)
|
|
r.firstHeader = true
|
|
break
|
|
}
|
|
out(w, topLevelHeader)
|
|
case 2:
|
|
out(w, secondLevelHdr)
|
|
default:
|
|
out(w, otherHeader)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) {
|
|
openTag := listTag
|
|
closeTag := listCloseTag
|
|
if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
|
|
// tags for definition lists handled within Item node
|
|
openTag = ""
|
|
closeTag = ""
|
|
}
|
|
if entering {
|
|
r.listDepth++
|
|
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
|
|
r.listCounters = append(r.listCounters, 1)
|
|
}
|
|
out(w, openTag)
|
|
} else {
|
|
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
|
|
r.listCounters = r.listCounters[:len(r.listCounters)-1]
|
|
}
|
|
out(w, closeTag)
|
|
r.listDepth--
|
|
}
|
|
}
|
|
|
|
func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) {
|
|
if entering {
|
|
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
|
|
out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1]))
|
|
r.listCounters[len(r.listCounters)-1]++
|
|
} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
|
|
// state machine for handling terms and following definitions
|
|
// since blackfriday does not distinguish them properly, nor
|
|
// does it seperate them into separate lists as it should
|
|
if !r.defineTerm {
|
|
out(w, arglistTag)
|
|
r.defineTerm = true
|
|
} else {
|
|
r.defineTerm = false
|
|
}
|
|
} else {
|
|
out(w, ".IP \\(bu 2\n")
|
|
}
|
|
} else {
|
|
out(w, "\n")
|
|
}
|
|
}
|
|
|
|
func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) {
|
|
if entering {
|
|
out(w, tableStart)
|
|
//call walker to count cells (and rows?) so format section can be produced
|
|
columns := countColumns(node)
|
|
out(w, strings.Repeat("l ", columns)+"\n")
|
|
out(w, strings.Repeat("l ", columns)+".\n")
|
|
} else {
|
|
out(w, tableEnd)
|
|
}
|
|
}
|
|
|
|
func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) {
|
|
var (
|
|
start, end string
|
|
)
|
|
if node.IsHeader {
|
|
start = codespanTag
|
|
end = codespanCloseTag
|
|
}
|
|
if entering {
|
|
if node.Prev != nil && node.Prev.Type == blackfriday.TableCell {
|
|
out(w, "\t"+start)
|
|
} else {
|
|
out(w, start)
|
|
}
|
|
} else {
|
|
// need to carriage return if we are at the end of the header row
|
|
if node.IsHeader && node.Next == nil {
|
|
end = end + crTag
|
|
}
|
|
out(w, end)
|
|
}
|
|
}
|
|
|
|
// because roff format requires knowing the column count before outputting any table
|
|
// data we need to walk a table tree and count the columns
|
|
func countColumns(node *blackfriday.Node) int {
|
|
var columns int
|
|
|
|
node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
|
switch node.Type {
|
|
case blackfriday.TableRow:
|
|
if !entering {
|
|
return blackfriday.Terminate
|
|
}
|
|
case blackfriday.TableCell:
|
|
if entering {
|
|
columns++
|
|
}
|
|
default:
|
|
}
|
|
return blackfriday.GoToNext
|
|
})
|
|
return columns
|
|
}
|
|
|
|
func out(w io.Writer, output string) {
|
|
io.WriteString(w, output) // nolint: errcheck
|
|
}
|
|
|
|
func needsBackslash(c byte) bool {
|
|
for _, r := range []byte("-_&\\~") {
|
|
if c == r {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func escapeSpecialChars(w io.Writer, text []byte) {
|
|
for i := 0; i < len(text); i++ {
|
|
// escape initial apostrophe or period
|
|
if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
|
|
out(w, "\\&")
|
|
}
|
|
|
|
// directly copy normal characters
|
|
org := i
|
|
|
|
for i < len(text) && !needsBackslash(text[i]) {
|
|
i++
|
|
}
|
|
if i > org {
|
|
w.Write(text[org:i]) // nolint: errcheck
|
|
}
|
|
|
|
// escape a character
|
|
if i >= len(text) {
|
|
break
|
|
}
|
|
|
|
w.Write([]byte{'\\', text[i]}) // nolint: errcheck
|
|
}
|
|
}
|