package md2man import ( "bufio" "bytes" "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 { listCounters []int firstHeader 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" codespanCloseTag = "\\fR" codeTag = "\n.EX\n" codeCloseTag = ".EE\n" // Do not prepend a newline character since code blocks, by definition, include a newline already (or at least as how blackfriday gives us on). quoteTag = "\n.PP\n.RS\n" quoteCloseTag = "\n.RE\n" listTag = "\n.RS\n" listCloseTag = ".RE\n" dtTag = "\n.TP\n" dd2Tag = "\n" tableStart = "\n.TS\nallbox;\n" tableEnd = ".TE\n" tableCellStart = "T{\n" tableCellEnd = "\nT}\n" tablePreprocessor = `'\" t` ) // NewRoffRenderer creates a new blackfriday Renderer for generating roff documents // from markdown func NewRoffRenderer() *roffRenderer { // nolint: golint return &roffRenderer{} } // GetExtensions returns the list of extensions used by this renderer implementation func (*roffRenderer) GetExtensions() blackfriday.Extensions { return blackfriday.NoIntraEmphasis | blackfriday.Tables | blackfriday.FencedCode | blackfriday.SpaceHeadings | blackfriday.Footnotes | blackfriday.Titleblock | blackfriday.DefinitionLists } // RenderHeader handles outputting the header at document start func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { // We need to walk the tree to check if there are any tables. // If there are, we need to enable the roff table preprocessor. ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if node.Type == blackfriday.Table { out(w, tablePreprocessor+"\n") return blackfriday.Terminate } return blackfriday.GoToNext }) // 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 { walkAction := blackfriday.GoToNext switch node.Type { case blackfriday.Text: // Special case: format the NAME section as required for proper whatis parsing. // Refer to the lexgrog(1) and groff_man(7) manual pages for details. if node.Parent != nil && node.Parent.Type == blackfriday.Paragraph && node.Parent.Prev != nil && node.Parent.Prev.Type == blackfriday.Heading && node.Parent.Prev.FirstChild != nil && bytes.EqualFold(node.Parent.Prev.FirstChild.Literal, []byte("NAME")) { before, after, found := bytes.Cut(node.Literal, []byte(" - ")) escapeSpecialChars(w, before) if found { out(w, ` \- `) escapeSpecialChars(w, after) } } else { escapeSpecialChars(w, node.Literal) } 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: // Don't render the link text for automatic links, because this // will only duplicate the URL in the roff output. // See https://daringfireball.net/projects/markdown/syntax#autolink if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) { out(w, string(node.FirstChild.Literal)) } // Hyphens in a link must be escaped to avoid word-wrap in the rendered man page. escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-") out(w, linkTag+escapedLink+linkCloseTag) walkAction = blackfriday.SkipChildren 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: if entering { if r.listDepth > 0 { // roff .PP markers break lists if node.Prev != nil { // continued paragraph if node.Prev.Type == blackfriday.List && node.Prev.ListFlags&blackfriday.ListTypeDefinition == 0 { out(w, ".IP\n") } else { out(w, crTag) } } } else if node.Prev != nil && node.Prev.Type == blackfriday.Heading { out(w, crTag) } else { out(w, paraTag) } } else { if node.Next == nil || node.Next.Type != blackfriday.List { 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.TableHead: case blackfriday.TableBody: case blackfriday.TableRow: // no action as cell entries do all the nroff formatting return blackfriday.GoToNext case blackfriday.TableCell: r.handleTableCell(w, node, entering) case blackfriday.HTMLSpan: // ignore other HTML tags case blackfriday.HTMLBlock: if bytes.HasPrefix(node.Literal, []byte("