roff.go

  1package roff
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"strings"
  7	"time"
  8)
  9
 10const (
 11	// Title heading (Document structure macro)
 12	TitleHeading = `.TH %[1]s %[2]d "%[4]s" "%[3]s" "%[5]s"`
 13	// Paragraph macro
 14	Paragraph = "\n.PP"
 15	// Relative-indent start (Document structure macro)
 16	Indent = "\n.RS"
 17	// Relative-indent end (Document structure macro)
 18	IndentEnd = "\n.RE"
 19	// Indented paragraph
 20	IndentedParagraph = "\n.IP"
 21	// Section heading (Document structure macro)
 22	SectionHeading = "\n.SH %s"
 23	// Tagged paragraph
 24	TaggedParagraph = "\n.TP"
 25
 26	// Bold escape
 27	Bold = `\fB`
 28	// Italic escape
 29	Italic = `\fI`
 30	// Return to previous font setting
 31	PreviousFont = `\fP`
 32)
 33
 34// Document is a roff document.
 35type Document struct {
 36	buffer bytes.Buffer
 37}
 38
 39// NewDocument returns a new roff Document.
 40func NewDocument() *Document {
 41	return &Document{}
 42}
 43
 44// write writes the given text to the internal buffer. Following the roff docs,
 45// we prevent empty lines in its output, as that may mysteriously break some
 46// roff renderers.
 47func (tr *Document) writef(format string, args ...interface{}) {
 48	if bytes.HasSuffix(tr.buffer.Bytes(), []byte("\n")) &&
 49		strings.HasPrefix(format, "\n") {
 50		// prevent empty lines in output
 51		format = strings.TrimPrefix(format, "\n")
 52	}
 53
 54	fmt.Fprintf(&tr.buffer, format, args...)
 55}
 56
 57func (tr *Document) writelnf(format string, args ...interface{}) {
 58	tr.writef(format+"\n", args...)
 59}
 60
 61// Heading writes the title heading of the document.
 62func (tr *Document) Heading(section uint, title, description string, ts time.Time) {
 63	tr.writef(TitleHeading, strings.ToUpper(title), section, title, ts.Format("2006-01-02"), description)
 64}
 65
 66// Paragraph starts a new paragraph.
 67func (tr *Document) Paragraph() {
 68	tr.writelnf(Paragraph)
 69}
 70
 71// Indent increases the indentation level.
 72func (tr *Document) Indent(n int) {
 73	if n >= 0 {
 74		tr.writelnf(Indent+" %d", n)
 75	} else {
 76		tr.writelnf(Indent)
 77	}
 78}
 79
 80// IndentEnd decreases the indentation level.
 81func (tr *Document) IndentEnd() {
 82	tr.writelnf(IndentEnd)
 83}
 84
 85// TaggedParagraph starts a new tagged paragraph.
 86func (tr *Document) TaggedParagraph(indentation int) {
 87	if indentation >= 0 {
 88		tr.writelnf(TaggedParagraph+" %d", indentation)
 89	} else {
 90		tr.writelnf(TaggedParagraph)
 91	}
 92}
 93
 94// List writes a list item.
 95func (tr *Document) List(text string) {
 96	tr.writelnf(IndentedParagraph+" \\(bu 3\n%s", escapeText(strings.TrimSpace(text)))
 97}
 98
 99// Section writes a section heading.
100func (tr *Document) Section(text string) {
101	tr.writelnf(SectionHeading, strings.ToUpper(text))
102}
103
104// EndSection ends the current section.
105func (tr *Document) EndSection() {
106	tr.writelnf("")
107}
108
109// Text writes text.
110func (tr *Document) Text(text string) {
111	inList := false
112	for i, s := range strings.Split(text, "\n") {
113		if i > 0 && !inList {
114			// start a new paragraph if we're not in a list
115			tr.Paragraph()
116		}
117
118		if strings.HasPrefix(s, "*") {
119			// list item
120			if !inList {
121				// start a new indented list if we're not in one
122				tr.Indent(-1)
123				inList = true
124			}
125
126			tr.List(s[1:])
127		} else {
128			// regular text
129			if inList {
130				// end the list if we're in one
131				tr.IndentEnd()
132				inList = false
133			}
134
135			tr.writef(escapeText(s))
136		}
137	}
138}
139
140// TextBold writes text in bold.
141func (tr *Document) TextBold(text string) {
142	tr.writef(Bold)
143	tr.Text(text)
144	tr.writef(PreviousFont)
145}
146
147// TextItalic writes text in italic.
148func (tr *Document) TextItalic(text string) {
149	tr.writef(Italic)
150	tr.Text(text)
151	tr.writef(PreviousFont)
152}
153
154// String returns the roff document as a string.
155func (tr Document) String() string {
156	return tr.buffer.String()
157}
158
159func escapeText(s string) string {
160	s = strings.ReplaceAll(s, `\`, `\e`)
161	s = strings.ReplaceAll(s, ".", "\\&.")
162	return s
163}