vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go
changeset 260 445e01aede7e
parent 256 6d9efbef00a9
equal deleted inserted replaced
259:db4911b0c721 260:445e01aede7e
    13 // roff format (manpages) from markdown text
    13 // roff format (manpages) from markdown text
    14 type roffRenderer struct {
    14 type roffRenderer struct {
    15 	extensions   blackfriday.Extensions
    15 	extensions   blackfriday.Extensions
    16 	listCounters []int
    16 	listCounters []int
    17 	firstHeader  bool
    17 	firstHeader  bool
    18 	defineTerm   bool
    18 	firstDD      bool
    19 	listDepth    int
    19 	listDepth    int
    20 }
    20 }
    21 
    21 
    22 const (
    22 const (
    23 	titleHeader      = ".TH "
    23 	titleHeader      = ".TH "
    40 	codeCloseTag     = "\n.fi\n.RE\n"
    40 	codeCloseTag     = "\n.fi\n.RE\n"
    41 	quoteTag         = "\n.PP\n.RS\n"
    41 	quoteTag         = "\n.PP\n.RS\n"
    42 	quoteCloseTag    = "\n.RE\n"
    42 	quoteCloseTag    = "\n.RE\n"
    43 	listTag          = "\n.RS\n"
    43 	listTag          = "\n.RS\n"
    44 	listCloseTag     = "\n.RE\n"
    44 	listCloseTag     = "\n.RE\n"
    45 	arglistTag       = "\n.TP\n"
    45 	dtTag            = "\n.TP\n"
       
    46 	dd2Tag           = "\n"
    46 	tableStart       = "\n.TS\nallbox;\n"
    47 	tableStart       = "\n.TS\nallbox;\n"
    47 	tableEnd         = ".TE\n"
    48 	tableEnd         = ".TE\n"
    48 	tableCellStart   = "T{\n"
    49 	tableCellStart   = "T{\n"
    49 	tableCellEnd     = "\nT}\n"
    50 	tableCellEnd     = "\nT}\n"
    50 )
    51 )
    88 
    89 
    89 	var walkAction = blackfriday.GoToNext
    90 	var walkAction = blackfriday.GoToNext
    90 
    91 
    91 	switch node.Type {
    92 	switch node.Type {
    92 	case blackfriday.Text:
    93 	case blackfriday.Text:
    93 		r.handleText(w, node, entering)
    94 		escapeSpecialChars(w, node.Literal)
    94 	case blackfriday.Softbreak:
    95 	case blackfriday.Softbreak:
    95 		out(w, crTag)
    96 		out(w, crTag)
    96 	case blackfriday.Hardbreak:
    97 	case blackfriday.Hardbreak:
    97 		out(w, breakTag)
    98 		out(w, breakTag)
    98 	case blackfriday.Emph:
    99 	case blackfriday.Emph:
   148 		out(w, codeTag)
   149 		out(w, codeTag)
   149 		escapeSpecialChars(w, node.Literal)
   150 		escapeSpecialChars(w, node.Literal)
   150 		out(w, codeCloseTag)
   151 		out(w, codeCloseTag)
   151 	case blackfriday.Table:
   152 	case blackfriday.Table:
   152 		r.handleTable(w, node, entering)
   153 		r.handleTable(w, node, entering)
   153 	case blackfriday.TableCell:
       
   154 		r.handleTableCell(w, node, entering)
       
   155 	case blackfriday.TableHead:
   154 	case blackfriday.TableHead:
   156 	case blackfriday.TableBody:
   155 	case blackfriday.TableBody:
   157 	case blackfriday.TableRow:
   156 	case blackfriday.TableRow:
   158 		// no action as cell entries do all the nroff formatting
   157 		// no action as cell entries do all the nroff formatting
   159 		return blackfriday.GoToNext
   158 		return blackfriday.GoToNext
       
   159 	case blackfriday.TableCell:
       
   160 		r.handleTableCell(w, node, entering)
       
   161 	case blackfriday.HTMLSpan:
       
   162 		// ignore other HTML tags
   160 	default:
   163 	default:
   161 		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
   164 		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
   162 	}
   165 	}
   163 	return walkAction
   166 	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 }
   167 }
   186 
   168 
   187 func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) {
   169 func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) {
   188 	if entering {
   170 	if entering {
   189 		switch node.Level {
   171 		switch node.Level {
   228 func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) {
   210 func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) {
   229 	if entering {
   211 	if entering {
   230 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
   212 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
   231 			out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1]))
   213 			out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1]))
   232 			r.listCounters[len(r.listCounters)-1]++
   214 			r.listCounters[len(r.listCounters)-1]++
       
   215 		} else if node.ListFlags&blackfriday.ListTypeTerm != 0 {
       
   216 			// DT (definition term): line just before DD (see below).
       
   217 			out(w, dtTag)
       
   218 			r.firstDD = true
   233 		} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
   219 		} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
   234 			// state machine for handling terms and following definitions
   220 			// DD (definition description): line that starts with ": ".
   235 			// since blackfriday does not distinguish them properly, nor
   221 			//
   236 			// does it seperate them into separate lists as it should
   222 			// We have to distinguish between the first DD and the
   237 			if !r.defineTerm {
   223 			// subsequent ones, as there should be no vertical
   238 				out(w, arglistTag)
   224 			// whitespace between the DT and the first DD.
   239 				r.defineTerm = true
   225 			if r.firstDD {
       
   226 				r.firstDD = false
   240 			} else {
   227 			} else {
   241 				r.defineTerm = false
   228 				out(w, dd2Tag)
   242 			}
   229 			}
   243 		} else {
   230 		} else {
   244 			out(w, ".IP \\(bu 2\n")
   231 			out(w, ".IP \\(bu 2\n")
   245 		}
   232 		}
   246 	} else {
   233 	} else {
   249 }
   236 }
   250 
   237 
   251 func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) {
   238 func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) {
   252 	if entering {
   239 	if entering {
   253 		out(w, tableStart)
   240 		out(w, tableStart)
   254 		//call walker to count cells (and rows?) so format section can be produced
   241 		// call walker to count cells (and rows?) so format section can be produced
   255 		columns := countColumns(node)
   242 		columns := countColumns(node)
   256 		out(w, strings.Repeat("l ", columns)+"\n")
   243 		out(w, strings.Repeat("l ", columns)+"\n")
   257 		out(w, strings.Repeat("l ", columns)+".\n")
   244 		out(w, strings.Repeat("l ", columns)+".\n")
   258 	} else {
   245 	} else {
   259 		out(w, tableEnd)
   246 		out(w, tableEnd)
   260 	}
   247 	}
   261 }
   248 }
   262 
   249 
   263 func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) {
   250 func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) {
   264 	var (
   251 	if entering {
   265 		start, end string
   252 		var start 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 {
   253 		if node.Prev != nil && node.Prev.Type == blackfriday.TableCell {
   273 			out(w, "\t"+start)
   254 			start = "\t"
   274 		} else {
   255 		}
   275 			out(w, start)
   256 		if node.IsHeader {
   276 		}
   257 			start += codespanTag
       
   258 		} else if nodeLiteralSize(node) > 30 {
       
   259 			start += tableCellStart
       
   260 		}
       
   261 		out(w, start)
   277 	} else {
   262 	} else {
   278 		// need to carriage return if we are at the end of the header row
   263 		var end string
   279 		if node.IsHeader && node.Next == nil {
   264 		if node.IsHeader {
   280 			end = end + crTag
   265 			end = codespanCloseTag
       
   266 		} else if nodeLiteralSize(node) > 30 {
       
   267 			end = tableCellEnd
       
   268 		}
       
   269 		if node.Next == nil && end != tableCellEnd {
       
   270 			// Last cell: need to carriage return if we are at the end of the
       
   271 			// header row and content isn't wrapped in a "tablecell"
       
   272 			end += crTag
   281 		}
   273 		}
   282 		out(w, end)
   274 		out(w, end)
   283 	}
   275 	}
       
   276 }
       
   277 
       
   278 func nodeLiteralSize(node *blackfriday.Node) int {
       
   279 	total := 0
       
   280 	for n := node.FirstChild; n != nil; n = n.FirstChild {
       
   281 		total += len(n.Literal)
       
   282 	}
       
   283 	return total
   284 }
   284 }
   285 
   285 
   286 // because roff format requires knowing the column count before outputting any table
   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
   287 // data we need to walk a table tree and count the columns
   288 func countColumns(node *blackfriday.Node) int {
   288 func countColumns(node *blackfriday.Node) int {
   307 
   307 
   308 func out(w io.Writer, output string) {
   308 func out(w io.Writer, output string) {
   309 	io.WriteString(w, output) // nolint: errcheck
   309 	io.WriteString(w, output) // nolint: errcheck
   310 }
   310 }
   311 
   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) {
   312 func escapeSpecialChars(w io.Writer, text []byte) {
   322 	for i := 0; i < len(text); i++ {
   313 	for i := 0; i < len(text); i++ {
   323 		// escape initial apostrophe or period
   314 		// escape initial apostrophe or period
   324 		if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
   315 		if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
   325 			out(w, "\\&")
   316 			out(w, "\\&")
   326 		}
   317 		}
   327 
   318 
   328 		// directly copy normal characters
   319 		// directly copy normal characters
   329 		org := i
   320 		org := i
   330 
   321 
   331 		for i < len(text) && !needsBackslash(text[i]) {
   322 		for i < len(text) && text[i] != '\\' {
   332 			i++
   323 			i++
   333 		}
   324 		}
   334 		if i > org {
   325 		if i > org {
   335 			w.Write(text[org:i]) // nolint: errcheck
   326 			w.Write(text[org:i]) // nolint: errcheck
   336 		}
   327 		}