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
15
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"
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
57
58 func NewRoffRenderer() *roffRenderer {
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
74 func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
75 return r.extensions
76 }
77
78
79 func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
80
81
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
91 out(w, ".nh\n")
92 }
93
94
95
96 func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
97 }
98
99
100
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
125
126
127 if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) {
128 out(w, string(node.FirstChild.Literal))
129 }
130
131 escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-")
132 out(w, linkTag+escapedLink+linkCloseTag)
133 walkAction = blackfriday.SkipChildren
134 case blackfriday.Image:
135
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
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
177 return blackfriday.GoToNext
178 case blackfriday.TableCell:
179 r.handleTableCell(w, node, entering)
180 case blackfriday.HTMLSpan:
181
182 case blackfriday.HTMLBlock:
183 if bytes.HasPrefix(node.Literal, []byte("<!--")) {
184 break
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
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
241 out(w, dtTag)
242 r.firstDD = true
243 } else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
244
245
246
247
248
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
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
295
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
311
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)
334 }
335
336 func escapeSpecialChars(w io.Writer, text []byte) {
337 scanner := bufio.NewScanner(bytes.NewReader(text))
338
339
340
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
361 if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
362 out(w, "\\&")
363 }
364
365
366 org := i
367
368 for i < len(text) && text[i] != '\\' {
369 i++
370 }
371 if i > org {
372 w.Write(text[org:i])
373 }
374
375
376 if i >= len(text) {
377 break
378 }
379
380 w.Write([]byte{'\\', text[i]})
381 }
382 }
383
View as plain text