256
|
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 |
260
|
18 |
firstDD bool |
256
|
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" |
260
|
45 |
dtTag = "\n.TP\n" |
|
46 |
dd2Tag = "\n" |
256
|
47 |
tableStart = "\n.TS\nallbox;\n" |
|
48 |
tableEnd = ".TE\n" |
|
49 |
tableCellStart = "T{\n" |
|
50 |
tableCellEnd = "\nT}\n" |
|
51 |
) |
|
52 |
|
|
53 |
// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents |
|
54 |
// from markdown |
|
55 |
func NewRoffRenderer() *roffRenderer { // nolint: golint |
|
56 |
var extensions blackfriday.Extensions |
|
57 |
|
|
58 |
extensions |= blackfriday.NoIntraEmphasis |
|
59 |
extensions |= blackfriday.Tables |
|
60 |
extensions |= blackfriday.FencedCode |
|
61 |
extensions |= blackfriday.SpaceHeadings |
|
62 |
extensions |= blackfriday.Footnotes |
|
63 |
extensions |= blackfriday.Titleblock |
|
64 |
extensions |= blackfriday.DefinitionLists |
|
65 |
return &roffRenderer{ |
|
66 |
extensions: extensions, |
|
67 |
} |
|
68 |
} |
|
69 |
|
|
70 |
// GetExtensions returns the list of extensions used by this renderer implementation |
|
71 |
func (r *roffRenderer) GetExtensions() blackfriday.Extensions { |
|
72 |
return r.extensions |
|
73 |
} |
|
74 |
|
|
75 |
// RenderHeader handles outputting the header at document start |
|
76 |
func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { |
|
77 |
// disable hyphenation |
|
78 |
out(w, ".nh\n") |
|
79 |
} |
|
80 |
|
|
81 |
// RenderFooter handles outputting the footer at the document end; the roff |
|
82 |
// renderer has no footer information |
|
83 |
func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { |
|
84 |
} |
|
85 |
|
|
86 |
// RenderNode is called for each node in a markdown document; based on the node |
|
87 |
// type the equivalent roff output is sent to the writer |
|
88 |
func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { |
|
89 |
|
|
90 |
var walkAction = blackfriday.GoToNext |
|
91 |
|
|
92 |
switch node.Type { |
|
93 |
case blackfriday.Text: |
260
|
94 |
escapeSpecialChars(w, node.Literal) |
256
|
95 |
case blackfriday.Softbreak: |
|
96 |
out(w, crTag) |
|
97 |
case blackfriday.Hardbreak: |
|
98 |
out(w, breakTag) |
|
99 |
case blackfriday.Emph: |
|
100 |
if entering { |
|
101 |
out(w, emphTag) |
|
102 |
} else { |
|
103 |
out(w, emphCloseTag) |
|
104 |
} |
|
105 |
case blackfriday.Strong: |
|
106 |
if entering { |
|
107 |
out(w, strongTag) |
|
108 |
} else { |
|
109 |
out(w, strongCloseTag) |
|
110 |
} |
|
111 |
case blackfriday.Link: |
|
112 |
if !entering { |
|
113 |
out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag) |
|
114 |
} |
|
115 |
case blackfriday.Image: |
|
116 |
// ignore images |
|
117 |
walkAction = blackfriday.SkipChildren |
|
118 |
case blackfriday.Code: |
|
119 |
out(w, codespanTag) |
|
120 |
escapeSpecialChars(w, node.Literal) |
|
121 |
out(w, codespanCloseTag) |
|
122 |
case blackfriday.Document: |
|
123 |
break |
|
124 |
case blackfriday.Paragraph: |
|
125 |
// roff .PP markers break lists |
|
126 |
if r.listDepth > 0 { |
|
127 |
return blackfriday.GoToNext |
|
128 |
} |
|
129 |
if entering { |
|
130 |
out(w, paraTag) |
|
131 |
} else { |
|
132 |
out(w, crTag) |
|
133 |
} |
|
134 |
case blackfriday.BlockQuote: |
|
135 |
if entering { |
|
136 |
out(w, quoteTag) |
|
137 |
} else { |
|
138 |
out(w, quoteCloseTag) |
|
139 |
} |
|
140 |
case blackfriday.Heading: |
|
141 |
r.handleHeading(w, node, entering) |
|
142 |
case blackfriday.HorizontalRule: |
|
143 |
out(w, hruleTag) |
|
144 |
case blackfriday.List: |
|
145 |
r.handleList(w, node, entering) |
|
146 |
case blackfriday.Item: |
|
147 |
r.handleItem(w, node, entering) |
|
148 |
case blackfriday.CodeBlock: |
|
149 |
out(w, codeTag) |
|
150 |
escapeSpecialChars(w, node.Literal) |
|
151 |
out(w, codeCloseTag) |
|
152 |
case blackfriday.Table: |
|
153 |
r.handleTable(w, node, entering) |
|
154 |
case blackfriday.TableHead: |
|
155 |
case blackfriday.TableBody: |
|
156 |
case blackfriday.TableRow: |
|
157 |
// no action as cell entries do all the nroff formatting |
|
158 |
return blackfriday.GoToNext |
260
|
159 |
case blackfriday.TableCell: |
|
160 |
r.handleTableCell(w, node, entering) |
|
161 |
case blackfriday.HTMLSpan: |
|
162 |
// ignore other HTML tags |
256
|
163 |
default: |
|
164 |
fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String()) |
|
165 |
} |
|
166 |
return walkAction |
|
167 |
} |
|
168 |
|
|
169 |
func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) { |
|
170 |
if entering { |
|
171 |
switch node.Level { |
|
172 |
case 1: |
|
173 |
if !r.firstHeader { |
|
174 |
out(w, titleHeader) |
|
175 |
r.firstHeader = true |
|
176 |
break |
|
177 |
} |
|
178 |
out(w, topLevelHeader) |
|
179 |
case 2: |
|
180 |
out(w, secondLevelHdr) |
|
181 |
default: |
|
182 |
out(w, otherHeader) |
|
183 |
} |
|
184 |
} |
|
185 |
} |
|
186 |
|
|
187 |
func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) { |
|
188 |
openTag := listTag |
|
189 |
closeTag := listCloseTag |
|
190 |
if node.ListFlags&blackfriday.ListTypeDefinition != 0 { |
|
191 |
// tags for definition lists handled within Item node |
|
192 |
openTag = "" |
|
193 |
closeTag = "" |
|
194 |
} |
|
195 |
if entering { |
|
196 |
r.listDepth++ |
|
197 |
if node.ListFlags&blackfriday.ListTypeOrdered != 0 { |
|
198 |
r.listCounters = append(r.listCounters, 1) |
|
199 |
} |
|
200 |
out(w, openTag) |
|
201 |
} else { |
|
202 |
if node.ListFlags&blackfriday.ListTypeOrdered != 0 { |
|
203 |
r.listCounters = r.listCounters[:len(r.listCounters)-1] |
|
204 |
} |
|
205 |
out(w, closeTag) |
|
206 |
r.listDepth-- |
|
207 |
} |
|
208 |
} |
|
209 |
|
|
210 |
func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) { |
|
211 |
if entering { |
|
212 |
if node.ListFlags&blackfriday.ListTypeOrdered != 0 { |
|
213 |
out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1])) |
|
214 |
r.listCounters[len(r.listCounters)-1]++ |
260
|
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 |
256
|
219 |
} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 { |
260
|
220 |
// DD (definition description): line that starts with ": ". |
|
221 |
// |
|
222 |
// We have to distinguish between the first DD and the |
|
223 |
// subsequent ones, as there should be no vertical |
|
224 |
// whitespace between the DT and the first DD. |
|
225 |
if r.firstDD { |
|
226 |
r.firstDD = false |
256
|
227 |
} else { |
260
|
228 |
out(w, dd2Tag) |
256
|
229 |
} |
|
230 |
} else { |
|
231 |
out(w, ".IP \\(bu 2\n") |
|
232 |
} |
|
233 |
} else { |
|
234 |
out(w, "\n") |
|
235 |
} |
|
236 |
} |
|
237 |
|
|
238 |
func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) { |
|
239 |
if entering { |
|
240 |
out(w, tableStart) |
260
|
241 |
// call walker to count cells (and rows?) so format section can be produced |
256
|
242 |
columns := countColumns(node) |
|
243 |
out(w, strings.Repeat("l ", columns)+"\n") |
|
244 |
out(w, strings.Repeat("l ", columns)+".\n") |
|
245 |
} else { |
|
246 |
out(w, tableEnd) |
|
247 |
} |
|
248 |
} |
|
249 |
|
|
250 |
func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) { |
|
251 |
if entering { |
260
|
252 |
var start string |
256
|
253 |
if node.Prev != nil && node.Prev.Type == blackfriday.TableCell { |
260
|
254 |
start = "\t" |
|
255 |
} |
|
256 |
if node.IsHeader { |
|
257 |
start += codespanTag |
|
258 |
} else if nodeLiteralSize(node) > 30 { |
|
259 |
start += tableCellStart |
256
|
260 |
} |
260
|
261 |
out(w, start) |
256
|
262 |
} else { |
260
|
263 |
var end string |
|
264 |
if node.IsHeader { |
|
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 |
256
|
273 |
} |
|
274 |
out(w, end) |
|
275 |
} |
|
276 |
} |
|
277 |
|
260
|
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 |
} |
|
285 |
|
256
|
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 escapeSpecialChars(w io.Writer, text []byte) { |
|
313 |
for i := 0; i < len(text); i++ { |
|
314 |
// escape initial apostrophe or period |
|
315 |
if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') { |
|
316 |
out(w, "\\&") |
|
317 |
} |
|
318 |
|
|
319 |
// directly copy normal characters |
|
320 |
org := i |
|
321 |
|
260
|
322 |
for i < len(text) && text[i] != '\\' { |
256
|
323 |
i++ |
|
324 |
} |
|
325 |
if i > org { |
|
326 |
w.Write(text[org:i]) // nolint: errcheck |
|
327 |
} |
|
328 |
|
|
329 |
// escape a character |
|
330 |
if i >= len(text) { |
|
331 |
break |
|
332 |
} |
|
333 |
|
|
334 |
w.Write([]byte{'\\', text[i]}) // nolint: errcheck |
|
335 |
} |
|
336 |
} |