зеркало из https://github.com/mislav/hub.git
219 строки
5.6 KiB
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)
|
|
}
|
|
}
|