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