vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go
changeset 256 6d9efbef00a9
child 260 445e01aede7e
equal deleted inserted replaced
255:4f153a23adab 256:6d9efbef00a9
       
     1 package md2man
       
     2 
       
     3 import (
       
     4 	"fmt"
       
     5 	"io"
       
     6 	"os"
       
     7 	"strings"
       
     8 
       
     9 	"github.com/russross/blackfriday/v2"
       
    10 )
       
    11 
       
    12 // roffRenderer implements the blackfriday.Renderer interface for creating
       
    13 // roff format (manpages) from markdown text
       
    14 type roffRenderer struct {
       
    15 	extensions   blackfriday.Extensions
       
    16 	listCounters []int
       
    17 	firstHeader  bool
       
    18 	defineTerm   bool
       
    19 	listDepth    int
       
    20 }
       
    21 
       
    22 const (
       
    23 	titleHeader      = ".TH "
       
    24 	topLevelHeader   = "\n\n.SH "
       
    25 	secondLevelHdr   = "\n.SH "
       
    26 	otherHeader      = "\n.SS "
       
    27 	crTag            = "\n"
       
    28 	emphTag          = "\\fI"
       
    29 	emphCloseTag     = "\\fP"
       
    30 	strongTag        = "\\fB"
       
    31 	strongCloseTag   = "\\fP"
       
    32 	breakTag         = "\n.br\n"
       
    33 	paraTag          = "\n.PP\n"
       
    34 	hruleTag         = "\n.ti 0\n\\l'\\n(.lu'\n"
       
    35 	linkTag          = "\n\\[la]"
       
    36 	linkCloseTag     = "\\[ra]"
       
    37 	codespanTag      = "\\fB\\fC"
       
    38 	codespanCloseTag = "\\fR"
       
    39 	codeTag          = "\n.PP\n.RS\n\n.nf\n"
       
    40 	codeCloseTag     = "\n.fi\n.RE\n"
       
    41 	quoteTag         = "\n.PP\n.RS\n"
       
    42 	quoteCloseTag    = "\n.RE\n"
       
    43 	listTag          = "\n.RS\n"
       
    44 	listCloseTag     = "\n.RE\n"
       
    45 	arglistTag       = "\n.TP\n"
       
    46 	tableStart       = "\n.TS\nallbox;\n"
       
    47 	tableEnd         = ".TE\n"
       
    48 	tableCellStart   = "T{\n"
       
    49 	tableCellEnd     = "\nT}\n"
       
    50 )
       
    51 
       
    52 // NewRoffRenderer creates a new blackfriday Renderer for generating roff documents
       
    53 // from markdown
       
    54 func NewRoffRenderer() *roffRenderer { // nolint: golint
       
    55 	var extensions blackfriday.Extensions
       
    56 
       
    57 	extensions |= blackfriday.NoIntraEmphasis
       
    58 	extensions |= blackfriday.Tables
       
    59 	extensions |= blackfriday.FencedCode
       
    60 	extensions |= blackfriday.SpaceHeadings
       
    61 	extensions |= blackfriday.Footnotes
       
    62 	extensions |= blackfriday.Titleblock
       
    63 	extensions |= blackfriday.DefinitionLists
       
    64 	return &roffRenderer{
       
    65 		extensions: extensions,
       
    66 	}
       
    67 }
       
    68 
       
    69 // GetExtensions returns the list of extensions used by this renderer implementation
       
    70 func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
       
    71 	return r.extensions
       
    72 }
       
    73 
       
    74 // RenderHeader handles outputting the header at document start
       
    75 func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
       
    76 	// disable hyphenation
       
    77 	out(w, ".nh\n")
       
    78 }
       
    79 
       
    80 // RenderFooter handles outputting the footer at the document end; the roff
       
    81 // renderer has no footer information
       
    82 func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
       
    83 }
       
    84 
       
    85 // RenderNode is called for each node in a markdown document; based on the node
       
    86 // type the equivalent roff output is sent to the writer
       
    87 func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
       
    88 
       
    89 	var walkAction = blackfriday.GoToNext
       
    90 
       
    91 	switch node.Type {
       
    92 	case blackfriday.Text:
       
    93 		r.handleText(w, node, entering)
       
    94 	case blackfriday.Softbreak:
       
    95 		out(w, crTag)
       
    96 	case blackfriday.Hardbreak:
       
    97 		out(w, breakTag)
       
    98 	case blackfriday.Emph:
       
    99 		if entering {
       
   100 			out(w, emphTag)
       
   101 		} else {
       
   102 			out(w, emphCloseTag)
       
   103 		}
       
   104 	case blackfriday.Strong:
       
   105 		if entering {
       
   106 			out(w, strongTag)
       
   107 		} else {
       
   108 			out(w, strongCloseTag)
       
   109 		}
       
   110 	case blackfriday.Link:
       
   111 		if !entering {
       
   112 			out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag)
       
   113 		}
       
   114 	case blackfriday.Image:
       
   115 		// ignore images
       
   116 		walkAction = blackfriday.SkipChildren
       
   117 	case blackfriday.Code:
       
   118 		out(w, codespanTag)
       
   119 		escapeSpecialChars(w, node.Literal)
       
   120 		out(w, codespanCloseTag)
       
   121 	case blackfriday.Document:
       
   122 		break
       
   123 	case blackfriday.Paragraph:
       
   124 		// roff .PP markers break lists
       
   125 		if r.listDepth > 0 {
       
   126 			return blackfriday.GoToNext
       
   127 		}
       
   128 		if entering {
       
   129 			out(w, paraTag)
       
   130 		} else {
       
   131 			out(w, crTag)
       
   132 		}
       
   133 	case blackfriday.BlockQuote:
       
   134 		if entering {
       
   135 			out(w, quoteTag)
       
   136 		} else {
       
   137 			out(w, quoteCloseTag)
       
   138 		}
       
   139 	case blackfriday.Heading:
       
   140 		r.handleHeading(w, node, entering)
       
   141 	case blackfriday.HorizontalRule:
       
   142 		out(w, hruleTag)
       
   143 	case blackfriday.List:
       
   144 		r.handleList(w, node, entering)
       
   145 	case blackfriday.Item:
       
   146 		r.handleItem(w, node, entering)
       
   147 	case blackfriday.CodeBlock:
       
   148 		out(w, codeTag)
       
   149 		escapeSpecialChars(w, node.Literal)
       
   150 		out(w, codeCloseTag)
       
   151 	case blackfriday.Table:
       
   152 		r.handleTable(w, node, entering)
       
   153 	case blackfriday.TableCell:
       
   154 		r.handleTableCell(w, node, entering)
       
   155 	case blackfriday.TableHead:
       
   156 	case blackfriday.TableBody:
       
   157 	case blackfriday.TableRow:
       
   158 		// no action as cell entries do all the nroff formatting
       
   159 		return blackfriday.GoToNext
       
   160 	default:
       
   161 		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
       
   162 	}
       
   163 	return walkAction
       
   164 }
       
   165 
       
   166 func (r *roffRenderer) handleText(w io.Writer, node *blackfriday.Node, entering bool) {
       
   167 	var (
       
   168 		start, end string
       
   169 	)
       
   170 	// handle special roff table cell text encapsulation
       
   171 	if node.Parent.Type == blackfriday.TableCell {
       
   172 		if len(node.Literal) > 30 {
       
   173 			start = tableCellStart
       
   174 			end = tableCellEnd
       
   175 		} else {
       
   176 			// end rows that aren't terminated by "tableCellEnd" with a cr if end of row
       
   177 			if node.Parent.Next == nil && !node.Parent.IsHeader {
       
   178 				end = crTag
       
   179 			}
       
   180 		}
       
   181 	}
       
   182 	out(w, start)
       
   183 	escapeSpecialChars(w, node.Literal)
       
   184 	out(w, end)
       
   185 }
       
   186 
       
   187 func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) {
       
   188 	if entering {
       
   189 		switch node.Level {
       
   190 		case 1:
       
   191 			if !r.firstHeader {
       
   192 				out(w, titleHeader)
       
   193 				r.firstHeader = true
       
   194 				break
       
   195 			}
       
   196 			out(w, topLevelHeader)
       
   197 		case 2:
       
   198 			out(w, secondLevelHdr)
       
   199 		default:
       
   200 			out(w, otherHeader)
       
   201 		}
       
   202 	}
       
   203 }
       
   204 
       
   205 func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) {
       
   206 	openTag := listTag
       
   207 	closeTag := listCloseTag
       
   208 	if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
       
   209 		// tags for definition lists handled within Item node
       
   210 		openTag = ""
       
   211 		closeTag = ""
       
   212 	}
       
   213 	if entering {
       
   214 		r.listDepth++
       
   215 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
       
   216 			r.listCounters = append(r.listCounters, 1)
       
   217 		}
       
   218 		out(w, openTag)
       
   219 	} else {
       
   220 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
       
   221 			r.listCounters = r.listCounters[:len(r.listCounters)-1]
       
   222 		}
       
   223 		out(w, closeTag)
       
   224 		r.listDepth--
       
   225 	}
       
   226 }
       
   227 
       
   228 func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) {
       
   229 	if entering {
       
   230 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
       
   231 			out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1]))
       
   232 			r.listCounters[len(r.listCounters)-1]++
       
   233 		} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
       
   234 			// state machine for handling terms and following definitions
       
   235 			// since blackfriday does not distinguish them properly, nor
       
   236 			// does it seperate them into separate lists as it should
       
   237 			if !r.defineTerm {
       
   238 				out(w, arglistTag)
       
   239 				r.defineTerm = true
       
   240 			} else {
       
   241 				r.defineTerm = false
       
   242 			}
       
   243 		} else {
       
   244 			out(w, ".IP \\(bu 2\n")
       
   245 		}
       
   246 	} else {
       
   247 		out(w, "\n")
       
   248 	}
       
   249 }
       
   250 
       
   251 func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) {
       
   252 	if entering {
       
   253 		out(w, tableStart)
       
   254 		//call walker to count cells (and rows?) so format section can be produced
       
   255 		columns := countColumns(node)
       
   256 		out(w, strings.Repeat("l ", columns)+"\n")
       
   257 		out(w, strings.Repeat("l ", columns)+".\n")
       
   258 	} else {
       
   259 		out(w, tableEnd)
       
   260 	}
       
   261 }
       
   262 
       
   263 func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) {
       
   264 	var (
       
   265 		start, end string
       
   266 	)
       
   267 	if node.IsHeader {
       
   268 		start = codespanTag
       
   269 		end = codespanCloseTag
       
   270 	}
       
   271 	if entering {
       
   272 		if node.Prev != nil && node.Prev.Type == blackfriday.TableCell {
       
   273 			out(w, "\t"+start)
       
   274 		} else {
       
   275 			out(w, start)
       
   276 		}
       
   277 	} else {
       
   278 		// need to carriage return if we are at the end of the header row
       
   279 		if node.IsHeader && node.Next == nil {
       
   280 			end = end + crTag
       
   281 		}
       
   282 		out(w, end)
       
   283 	}
       
   284 }
       
   285 
       
   286 // because roff format requires knowing the column count before outputting any table
       
   287 // data we need to walk a table tree and count the columns
       
   288 func countColumns(node *blackfriday.Node) int {
       
   289 	var columns int
       
   290 
       
   291 	node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
       
   292 		switch node.Type {
       
   293 		case blackfriday.TableRow:
       
   294 			if !entering {
       
   295 				return blackfriday.Terminate
       
   296 			}
       
   297 		case blackfriday.TableCell:
       
   298 			if entering {
       
   299 				columns++
       
   300 			}
       
   301 		default:
       
   302 		}
       
   303 		return blackfriday.GoToNext
       
   304 	})
       
   305 	return columns
       
   306 }
       
   307 
       
   308 func out(w io.Writer, output string) {
       
   309 	io.WriteString(w, output) // nolint: errcheck
       
   310 }
       
   311 
       
   312 func needsBackslash(c byte) bool {
       
   313 	for _, r := range []byte("-_&\\~") {
       
   314 		if c == r {
       
   315 			return true
       
   316 		}
       
   317 	}
       
   318 	return false
       
   319 }
       
   320 
       
   321 func escapeSpecialChars(w io.Writer, text []byte) {
       
   322 	for i := 0; i < len(text); i++ {
       
   323 		// escape initial apostrophe or period
       
   324 		if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
       
   325 			out(w, "\\&")
       
   326 		}
       
   327 
       
   328 		// directly copy normal characters
       
   329 		org := i
       
   330 
       
   331 		for i < len(text) && !needsBackslash(text[i]) {
       
   332 			i++
       
   333 		}
       
   334 		if i > org {
       
   335 			w.Write(text[org:i]) // nolint: errcheck
       
   336 		}
       
   337 
       
   338 		// escape a character
       
   339 		if i >= len(text) {
       
   340 			break
       
   341 		}
       
   342 
       
   343 		w.Write([]byte{'\\', text[i]}) // nolint: errcheck
       
   344 	}
       
   345 }