1package md2man
2
3import (
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
14type roffRenderer struct {
15	extensions   blackfriday.Extensions
16	listCounters []int
17	firstHeader  bool
18	defineTerm   bool
19	listDepth    int
20}
21
22const (
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
54func 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
70func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
71	return r.extensions
72}
73
74// RenderHeader handles outputting the header at document start
75func (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
82func (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
87func (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
166func (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
187func (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
205func (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
228func (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
251func (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
263func (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
288func 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
308func out(w io.Writer, output string) {
309	io.WriteString(w, output) // nolint: errcheck
310}
311
312func needsBackslash(c byte) bool {
313	for _, r := range []byte("-_&\\~") {
314		if c == r {
315			return true
316		}
317	}
318	return false
319}
320
321func 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}
346