...

Source file src/github.com/cpuguy83/go-md2man/v2/md2man/roff.go

Documentation: github.com/cpuguy83/go-md2man/v2/md2man

     1  package md2man
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/russross/blackfriday/v2"
    12  )
    13  
    14  // roffRenderer implements the blackfriday.Renderer interface for creating
    15  // roff format (manpages) from markdown text
    16  type roffRenderer struct {
    17  	extensions   blackfriday.Extensions
    18  	listCounters []int
    19  	firstHeader  bool
    20  	firstDD      bool
    21  	listDepth    int
    22  }
    23  
    24  const (
    25  	titleHeader       = ".TH "
    26  	topLevelHeader    = "\n\n.SH "
    27  	secondLevelHdr    = "\n.SH "
    28  	otherHeader       = "\n.SS "
    29  	crTag             = "\n"
    30  	emphTag           = "\\fI"
    31  	emphCloseTag      = "\\fP"
    32  	strongTag         = "\\fB"
    33  	strongCloseTag    = "\\fP"
    34  	breakTag          = "\n.br\n"
    35  	paraTag           = "\n.PP\n"
    36  	hruleTag          = "\n.ti 0\n\\l'\\n(.lu'\n"
    37  	linkTag           = "\n\\[la]"
    38  	linkCloseTag      = "\\[ra]"
    39  	codespanTag       = "\\fB"
    40  	codespanCloseTag  = "\\fR"
    41  	codeTag           = "\n.EX\n"
    42  	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).
    43  	quoteTag          = "\n.PP\n.RS\n"
    44  	quoteCloseTag     = "\n.RE\n"
    45  	listTag           = "\n.RS\n"
    46  	listCloseTag      = "\n.RE\n"
    47  	dtTag             = "\n.TP\n"
    48  	dd2Tag            = "\n"
    49  	tableStart        = "\n.TS\nallbox;\n"
    50  	tableEnd          = ".TE\n"
    51  	tableCellStart    = "T{\n"
    52  	tableCellEnd      = "\nT}\n"
    53  	tablePreprocessor = `'\" t`
    54  )
    55  
    56  // NewRoffRenderer creates a new blackfriday Renderer for generating roff documents
    57  // from markdown
    58  func NewRoffRenderer() *roffRenderer { // nolint: golint
    59  	var extensions blackfriday.Extensions
    60  
    61  	extensions |= blackfriday.NoIntraEmphasis
    62  	extensions |= blackfriday.Tables
    63  	extensions |= blackfriday.FencedCode
    64  	extensions |= blackfriday.SpaceHeadings
    65  	extensions |= blackfriday.Footnotes
    66  	extensions |= blackfriday.Titleblock
    67  	extensions |= blackfriday.DefinitionLists
    68  	return &roffRenderer{
    69  		extensions: extensions,
    70  	}
    71  }
    72  
    73  // GetExtensions returns the list of extensions used by this renderer implementation
    74  func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
    75  	return r.extensions
    76  }
    77  
    78  // RenderHeader handles outputting the header at document start
    79  func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
    80  	// We need to walk the tree to check if there are any tables.
    81  	// If there are, we need to enable the roff table preprocessor.
    82  	ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
    83  		if node.Type == blackfriday.Table {
    84  			out(w, tablePreprocessor+"\n")
    85  			return blackfriday.Terminate
    86  		}
    87  		return blackfriday.GoToNext
    88  	})
    89  
    90  	// disable hyphenation
    91  	out(w, ".nh\n")
    92  }
    93  
    94  // RenderFooter handles outputting the footer at the document end; the roff
    95  // renderer has no footer information
    96  func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
    97  }
    98  
    99  // RenderNode is called for each node in a markdown document; based on the node
   100  // type the equivalent roff output is sent to the writer
   101  func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
   102  	walkAction := blackfriday.GoToNext
   103  
   104  	switch node.Type {
   105  	case blackfriday.Text:
   106  		escapeSpecialChars(w, node.Literal)
   107  	case blackfriday.Softbreak:
   108  		out(w, crTag)
   109  	case blackfriday.Hardbreak:
   110  		out(w, breakTag)
   111  	case blackfriday.Emph:
   112  		if entering {
   113  			out(w, emphTag)
   114  		} else {
   115  			out(w, emphCloseTag)
   116  		}
   117  	case blackfriday.Strong:
   118  		if entering {
   119  			out(w, strongTag)
   120  		} else {
   121  			out(w, strongCloseTag)
   122  		}
   123  	case blackfriday.Link:
   124  		// Don't render the link text for automatic links, because this
   125  		// will only duplicate the URL in the roff output.
   126  		// See https://daringfireball.net/projects/markdown/syntax#autolink
   127  		if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) {
   128  			out(w, string(node.FirstChild.Literal))
   129  		}
   130  		// Hyphens in a link must be escaped to avoid word-wrap in the rendered man page.
   131  		escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-")
   132  		out(w, linkTag+escapedLink+linkCloseTag)
   133  		walkAction = blackfriday.SkipChildren
   134  	case blackfriday.Image:
   135  		// ignore images
   136  		walkAction = blackfriday.SkipChildren
   137  	case blackfriday.Code:
   138  		out(w, codespanTag)
   139  		escapeSpecialChars(w, node.Literal)
   140  		out(w, codespanCloseTag)
   141  	case blackfriday.Document:
   142  		break
   143  	case blackfriday.Paragraph:
   144  		// roff .PP markers break lists
   145  		if r.listDepth > 0 {
   146  			return blackfriday.GoToNext
   147  		}
   148  		if entering {
   149  			out(w, paraTag)
   150  		} else {
   151  			out(w, crTag)
   152  		}
   153  	case blackfriday.BlockQuote:
   154  		if entering {
   155  			out(w, quoteTag)
   156  		} else {
   157  			out(w, quoteCloseTag)
   158  		}
   159  	case blackfriday.Heading:
   160  		r.handleHeading(w, node, entering)
   161  	case blackfriday.HorizontalRule:
   162  		out(w, hruleTag)
   163  	case blackfriday.List:
   164  		r.handleList(w, node, entering)
   165  	case blackfriday.Item:
   166  		r.handleItem(w, node, entering)
   167  	case blackfriday.CodeBlock:
   168  		out(w, codeTag)
   169  		escapeSpecialChars(w, node.Literal)
   170  		out(w, codeCloseTag)
   171  	case blackfriday.Table:
   172  		r.handleTable(w, node, entering)
   173  	case blackfriday.TableHead:
   174  	case blackfriday.TableBody:
   175  	case blackfriday.TableRow:
   176  		// no action as cell entries do all the nroff formatting
   177  		return blackfriday.GoToNext
   178  	case blackfriday.TableCell:
   179  		r.handleTableCell(w, node, entering)
   180  	case blackfriday.HTMLSpan:
   181  		// ignore other HTML tags
   182  	case blackfriday.HTMLBlock:
   183  		if bytes.HasPrefix(node.Literal, []byte("<!--")) {
   184  			break // ignore comments, no warning
   185  		}
   186  		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
   187  	default:
   188  		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
   189  	}
   190  	return walkAction
   191  }
   192  
   193  func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) {
   194  	if entering {
   195  		switch node.Level {
   196  		case 1:
   197  			if !r.firstHeader {
   198  				out(w, titleHeader)
   199  				r.firstHeader = true
   200  				break
   201  			}
   202  			out(w, topLevelHeader)
   203  		case 2:
   204  			out(w, secondLevelHdr)
   205  		default:
   206  			out(w, otherHeader)
   207  		}
   208  	}
   209  }
   210  
   211  func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) {
   212  	openTag := listTag
   213  	closeTag := listCloseTag
   214  	if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
   215  		// tags for definition lists handled within Item node
   216  		openTag = ""
   217  		closeTag = ""
   218  	}
   219  	if entering {
   220  		r.listDepth++
   221  		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
   222  			r.listCounters = append(r.listCounters, 1)
   223  		}
   224  		out(w, openTag)
   225  	} else {
   226  		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
   227  			r.listCounters = r.listCounters[:len(r.listCounters)-1]
   228  		}
   229  		out(w, closeTag)
   230  		r.listDepth--
   231  	}
   232  }
   233  
   234  func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) {
   235  	if entering {
   236  		if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
   237  			out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1]))
   238  			r.listCounters[len(r.listCounters)-1]++
   239  		} else if node.ListFlags&blackfriday.ListTypeTerm != 0 {
   240  			// DT (definition term): line just before DD (see below).
   241  			out(w, dtTag)
   242  			r.firstDD = true
   243  		} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
   244  			// DD (definition description): line that starts with ": ".
   245  			//
   246  			// We have to distinguish between the first DD and the
   247  			// subsequent ones, as there should be no vertical
   248  			// whitespace between the DT and the first DD.
   249  			if r.firstDD {
   250  				r.firstDD = false
   251  			} else {
   252  				out(w, dd2Tag)
   253  			}
   254  		} else {
   255  			out(w, ".IP \\(bu 2\n")
   256  		}
   257  	} else {
   258  		out(w, "\n")
   259  	}
   260  }
   261  
   262  func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) {
   263  	if entering {
   264  		out(w, tableStart)
   265  		// call walker to count cells (and rows?) so format section can be produced
   266  		columns := countColumns(node)
   267  		out(w, strings.Repeat("l ", columns)+"\n")
   268  		out(w, strings.Repeat("l ", columns)+".\n")
   269  	} else {
   270  		out(w, tableEnd)
   271  	}
   272  }
   273  
   274  func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) {
   275  	if entering {
   276  		var start string
   277  		if node.Prev != nil && node.Prev.Type == blackfriday.TableCell {
   278  			start = "\t"
   279  		}
   280  		if node.IsHeader {
   281  			start += strongTag
   282  		} else if nodeLiteralSize(node) > 30 {
   283  			start += tableCellStart
   284  		}
   285  		out(w, start)
   286  	} else {
   287  		var end string
   288  		if node.IsHeader {
   289  			end = strongCloseTag
   290  		} else if nodeLiteralSize(node) > 30 {
   291  			end = tableCellEnd
   292  		}
   293  		if node.Next == nil && end != tableCellEnd {
   294  			// Last cell: need to carriage return if we are at the end of the
   295  			// header row and content isn't wrapped in a "tablecell"
   296  			end += crTag
   297  		}
   298  		out(w, end)
   299  	}
   300  }
   301  
   302  func nodeLiteralSize(node *blackfriday.Node) int {
   303  	total := 0
   304  	for n := node.FirstChild; n != nil; n = n.FirstChild {
   305  		total += len(n.Literal)
   306  	}
   307  	return total
   308  }
   309  
   310  // because roff format requires knowing the column count before outputting any table
   311  // data we need to walk a table tree and count the columns
   312  func countColumns(node *blackfriday.Node) int {
   313  	var columns int
   314  
   315  	node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
   316  		switch node.Type {
   317  		case blackfriday.TableRow:
   318  			if !entering {
   319  				return blackfriday.Terminate
   320  			}
   321  		case blackfriday.TableCell:
   322  			if entering {
   323  				columns++
   324  			}
   325  		default:
   326  		}
   327  		return blackfriday.GoToNext
   328  	})
   329  	return columns
   330  }
   331  
   332  func out(w io.Writer, output string) {
   333  	io.WriteString(w, output) // nolint: errcheck
   334  }
   335  
   336  func escapeSpecialChars(w io.Writer, text []byte) {
   337  	scanner := bufio.NewScanner(bytes.NewReader(text))
   338  
   339  	// count the number of lines in the text
   340  	// we need to know this to avoid adding a newline after the last line
   341  	n := bytes.Count(text, []byte{'\n'})
   342  	idx := 0
   343  
   344  	for scanner.Scan() {
   345  		dt := scanner.Bytes()
   346  		if idx < n {
   347  			idx++
   348  			dt = append(dt, '\n')
   349  		}
   350  		escapeSpecialCharsLine(w, dt)
   351  	}
   352  
   353  	if err := scanner.Err(); err != nil {
   354  		panic(err)
   355  	}
   356  }
   357  
   358  func escapeSpecialCharsLine(w io.Writer, text []byte) {
   359  	for i := 0; i < len(text); i++ {
   360  		// escape initial apostrophe or period
   361  		if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
   362  			out(w, "\\&")
   363  		}
   364  
   365  		// directly copy normal characters
   366  		org := i
   367  
   368  		for i < len(text) && text[i] != '\\' {
   369  			i++
   370  		}
   371  		if i > org {
   372  			w.Write(text[org:i]) // nolint: errcheck
   373  		}
   374  
   375  		// escape a character
   376  		if i >= len(text) {
   377  			break
   378  		}
   379  
   380  		w.Write([]byte{'\\', text[i]}) // nolint: errcheck
   381  	}
   382  }
   383  

View as plain text