hub/md2roff/renderer.go

219 строки
5.6 KiB
Go

package md2roff
import (
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"github.com/russross/blackfriday"
)
// https://github.com/russross/blackfriday/blob/v2/markdown.go
const (
PARSER_EXTENSIONS = blackfriday.NoIntraEmphasis |
blackfriday.FencedCode |
blackfriday.SpaceHeadings |
blackfriday.AutoHeadingIDs |
blackfriday.DefinitionLists
)
var (
backslash = []byte{'\\'}
enterVar = []byte("<var>")
closeVar = []byte("</var>")
htmlEscape = regexp.MustCompile(`<([A-Za-z][A-Za-z0-9_-]*)>`)
roffEscape = regexp.MustCompile(`[&\~_-]`)
headingEscape = regexp.MustCompile(`["]`)
titleRe = regexp.MustCompile(`(?P<name>[A-Za-z][A-Za-z0-9_-]+)\((?P<num>\d)\) -- (?P<title>.+)`)
)
func escape(src []byte, re *regexp.Regexp) []byte {
return re.ReplaceAllFunc(src, func(c []byte) []byte {
return append(backslash, c...)
})
}
type RoffRenderer struct {
Manual string
Version string
Date string
Title string
Name string
Section uint8
listWasTerm bool
}
func (r *RoffRenderer) RenderHeader(buf io.Writer, ast *blackfriday.Node) {
}
func (r *RoffRenderer) RenderFooter(buf io.Writer, ast *blackfriday.Node) {
}
func (r *RoffRenderer) RenderNode(buf io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
if entering {
switch node.Type {
case blackfriday.Emph:
io.WriteString(buf, `\fI`)
case blackfriday.Strong:
io.WriteString(buf, `\fB`)
case blackfriday.Link:
io.WriteString(buf, `\[la]`)
case blackfriday.Code:
io.WriteString(buf, `\fB\fC`)
case blackfriday.Hardbreak:
io.WriteString(buf, "\n.br\n")
case blackfriday.Paragraph:
if node.Parent.Type != blackfriday.Item {
io.WriteString(buf, ".P\n")
} else if node.Parent.FirstChild != node {
io.WriteString(buf, ".sp\n")
if node.Prev.Type == blackfriday.List {
io.WriteString(buf, ".PP\n")
}
}
case blackfriday.CodeBlock:
io.WriteString(buf, ".PP\n.RS 4\n.nf\n")
case blackfriday.Item:
if node.ListFlags&blackfriday.ListTypeDefinition == 0 {
if node.Parent.ListData.Tight && node.Parent.FirstChild != node {
io.WriteString(buf, ".sp -1\n")
}
if node.Parent.ListData.Tight {
io.WriteString(buf, ".IP \\(bu 2.3\n")
} else {
io.WriteString(buf, ".IP \\(bu 4\n")
}
} else {
if node.ListFlags&blackfriday.ListTypeTerm != 0 {
io.WriteString(buf, ".PP\n")
} else {
io.WriteString(buf, ".RS 4\n")
}
}
case blackfriday.Heading:
r.renderHeading(buf, node)
return blackfriday.SkipChildren
}
}
leaf := len(node.Literal) > 0
if leaf {
if bytes.Compare(node.Literal, enterVar) == 0 {
io.WriteString(buf, `\fI`)
} else if bytes.Compare(node.Literal, closeVar) == 0 {
io.WriteString(buf, `\fP`)
} else {
buf.Write(escape(node.Literal, roffEscape))
}
}
if !entering || leaf {
switch node.Type {
case blackfriday.Emph,
blackfriday.Strong:
io.WriteString(buf, `\fP`)
case blackfriday.Link:
io.WriteString(buf, `\[ra]`)
case blackfriday.Code:
io.WriteString(buf, `\fR`)
case blackfriday.CodeBlock:
io.WriteString(buf, ".fi\n.RE\n")
case blackfriday.HTMLSpan,
blackfriday.Del,
blackfriday.Image:
case blackfriday.List:
io.WriteString(buf, ".br\n")
case blackfriday.Item:
if node.ListFlags&blackfriday.ListTypeDefinition != 0 &&
node.ListFlags&blackfriday.ListTypeTerm == 0 {
io.WriteString(buf, ".RE\n")
}
default:
if !leaf {
io.WriteString(buf, "\n")
}
}
}
return blackfriday.GoToNext
}
func textContent(node *blackfriday.Node) []byte {
var buf bytes.Buffer
node.Walk(func(n *blackfriday.Node, entering bool) blackfriday.WalkStatus {
if entering && len(n.Literal) > 0 {
buf.Write(n.Literal)
}
return blackfriday.GoToNext
})
return buf.Bytes()
}
func (r *RoffRenderer) renderHeading(buf io.Writer, node *blackfriday.Node) {
text := textContent(node)
switch node.HeadingData.Level {
case 1:
var name []byte
var num []byte
if match := titleRe.FindAllSubmatch(text, 1); match != nil {
name, num, text = match[0][1], match[0][2], match[0][3]
r.Name = string(name)
if sectionNum, err := strconv.Atoi(string(num)); err == nil {
r.Section = uint8(sectionNum)
}
r.Title = string(text)
}
fmt.Fprintf(buf, ".TH \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"\n",
escape(name, headingEscape),
num,
escape([]byte(r.Date), headingEscape),
escape([]byte(r.Version), headingEscape),
escape([]byte(r.Manual), headingEscape),
)
io.WriteString(buf, ".nh\n") // disable hyphenation
io.WriteString(buf, ".ad l\n") // disable justification
io.WriteString(buf, ".SH \"NAME\"\n")
fmt.Fprintf(buf, "%s \\- %s\n",
escape(name, roffEscape),
escape(text, roffEscape),
)
case 2:
fmt.Fprintf(buf, ".SH \"%s\"\n", strings.ToUpper(string(escape(text, headingEscape))))
case 3:
fmt.Fprintf(buf, ".SS \"%s\"\n", escape(text, headingEscape))
}
}
func sanitizeInput(src []byte) []byte {
return htmlEscape.ReplaceAllFunc(src, func(match []byte) []byte {
res := append(enterVar, match[1:len(match)-1]...)
return append(res, closeVar...)
})
}
type renderOption struct {
renderer blackfriday.Renderer
buffer io.Writer
}
func Opt(buffer io.Writer, renderer blackfriday.Renderer) *renderOption {
return &renderOption{renderer, buffer}
}
func Generate(src []byte, opts ...*renderOption) {
parser := blackfriday.New(blackfriday.WithExtensions(PARSER_EXTENSIONS))
ast := parser.Parse(sanitizeInput(src))
for _, opt := range opts {
opt.renderer.RenderHeader(opt.buffer, ast)
ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
return opt.renderer.RenderNode(opt.buffer, node, entering)
})
opt.renderer.RenderFooter(opt.buffer, ast)
}
}