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}