1package main
2
3import (
4 "fmt"
5 "io"
6 "strings"
7)
8
9// fixed-width ascii layout, matching the column widths a classic
10// nroff -Tascii rendering of an mdoc page would produce
11const (
12 txtWidth = 78
13 txtIndent = 7
14 txtTagIndent = 15
15 txtSubIndent = 4
16)
17
18type TxtRenderer struct {
19 page *Page
20 w io.Writer
21 wrote bool
22}
23
24func NewTxtRenderer(page *Page, w io.Writer) *TxtRenderer {
25 return &TxtRenderer{page: page, w: w}
26}
27
28func (r *TxtRenderer) Render() error {
29 if err := r.page.validate(); err != nil {
30 return err
31 }
32
33 r.heading("NAME")
34 r.writeIndented(r.page.Name+" - "+renderInlinesTxt(parseInlines(r.page.Summary)), txtIndent)
35
36 if len(r.page.Synopsis) != 0 {
37 r.renderSynopsis()
38 }
39 if len(r.page.Description) != 0 {
40 r.heading("DESCRIPTION")
41 for _, block := range r.page.Description {
42 r.renderBlock(block, txtIndent)
43 }
44 }
45 if len(r.page.Options) != 0 {
46 r.renderOptions()
47 }
48 for _, section := range r.page.Sections {
49 r.heading(section.Title)
50 for _, block := range section.Blocks {
51 r.renderBlock(block, txtIndent)
52 }
53 }
54 return nil
55}
56
57// heading prints a flush-left section title, with a blank line above
58// every one but the first
59func (r *TxtRenderer) heading(title string) {
60 if r.wrote {
61 fmt.Fprintln(r.w)
62 }
63 fmt.Fprintln(r.w, title)
64 r.wrote = true
65}
66
67func (r *TxtRenderer) renderSynopsis() {
68 r.heading("SYNOPSIS")
69 hang := len(r.page.Name) + 1
70
71 for _, form := range r.page.Synopsis {
72 body := renderSynopsisItemsTxt(form.Items)
73
74 avail := txtWidth - txtIndent - hang
75 if avail < 10 {
76 avail = 10
77 }
78 lines := wrapWords(body, avail)
79 if len(lines) == 0 {
80 lines = []string{""}
81 }
82
83 fmt.Fprintln(r.w, strings.Repeat(" ", txtIndent)+strings.TrimSpace(r.page.Name+" "+lines[0]))
84 for _, l := range lines[1:] {
85 fmt.Fprintln(r.w, strings.Repeat(" ", txtIndent+hang)+l)
86 }
87 }
88}
89
90func (r *TxtRenderer) renderOptions() {
91 r.heading("OPTIONS")
92 for i, opt := range r.page.Options {
93 if i != 0 {
94 fmt.Fprintln(r.w)
95 }
96 r.writeIndented(renderSynopsisItemsTxt(opt.Spec), txtIndent)
97 for _, block := range opt.Body {
98 r.renderBlock(block, txtTagIndent)
99 }
100 }
101}
102
103func (r *TxtRenderer) renderBlock(block Block, indent int) {
104 switch block.Kind {
105 case BlockParagraph:
106 fmt.Fprintln(r.w)
107 r.writeIndented(renderInlinesTxt(block.Inlines), indent)
108 case BlockTaggedList:
109 for i, item := range block.Items {
110 if i != 0 {
111 fmt.Fprintln(r.w)
112 }
113 r.writeIndented(renderInlinesTxt(item.Label), indent)
114 if len(item.Body) != 0 {
115 fmt.Fprintln(r.w)
116 r.writeIndented(renderInlinesTxt(item.Body), indent+8)
117 }
118 }
119 case BlockSeeAlso:
120 var refs []string
121 for _, ref := range block.Refs {
122 refs = append(refs, ref.Name+"("+ref.Section+")")
123 }
124 fmt.Fprintln(r.w)
125 r.writeIndented(strings.Join(refs, ", "), indent)
126 case BlockSubsection:
127 fmt.Fprintln(r.w)
128 r.writeIndented(block.Title, indent)
129 for _, child := range block.Blocks {
130 r.renderBlock(child, indent+txtSubIndent)
131 }
132 }
133}
134
135// writeIndented word-wraps text to the page width minus indent, then
136// writes each line prefixed by indent spaces
137func (r *TxtRenderer) writeIndented(text string, indent int) {
138 width := txtWidth - indent
139 if width < 10 {
140 width = 10
141 }
142 for _, line := range wrapWords(text, width) {
143 fmt.Fprintln(r.w, strings.Repeat(" ", indent)+line)
144 }
145}
146
147func wrapWords(text string, width int) []string {
148 text = strings.Join(strings.Fields(text), " ")
149 if text == "" {
150 return nil
151 }
152 if width < 1 {
153 width = 1
154 }
155
156 words := strings.Fields(text)
157 lines := []string{words[0]}
158 for _, word := range words[1:] {
159 last := lines[len(lines)-1]
160 if len(last)+1+len(word) <= width {
161 lines[len(lines)-1] = last + " " + word
162 } else {
163 lines = append(lines, word)
164 }
165 }
166 return lines
167}
168
169// renderSynopsisItemsTxt mirrors renderSynopsisPhrase from mdoc.go but
170// writes literal brackets and braces instead of mdoc macros
171func renderSynopsisItemsTxt(items []SynopsisItem) string {
172 var parts []string
173 for _, item := range items {
174 switch item.Kind {
175 case SynFlag:
176 parts = append(parts, "-"+strings.TrimPrefix(item.Text, "-"))
177 case SynArg:
178 parts = append(parts, strings.Trim(item.Text, "<>"))
179 case SynCommand:
180 parts = append(parts, item.Text)
181 case SynPipe:
182 parts = append(parts, "|")
183 case SynLiteral:
184 parts = append(parts, item.Text)
185 case SynOptional:
186 parts = append(parts, "["+renderSynopsisItemsTxt(item.Children)+"]")
187 case SynRequiredGroup:
188 parts = append(parts, "{"+renderSynopsisItemsTxt(item.Children)+"}")
189 }
190 }
191 return strings.Join(parts, " ")
192}
193
194func renderInlinesTxt(inlines []Inline) string {
195 var b strings.Builder
196 for _, inline := range inlines {
197 b.WriteString(renderInlineTxt(inline))
198 }
199 return b.String()
200}
201
202func renderInlineTxt(inline Inline) string {
203 switch inline.Kind {
204 case InlineText:
205 return inline.Text
206 case InlineNm:
207 return inline.Text
208 case InlineEmph:
209 return "_" + renderInlinesTxt(inline.Children) + "_"
210 case InlineLiteral:
211 return "`" + inline.Text + "'"
212 case InlinePath:
213 return inline.Text
214 case InlineXRef:
215 return inline.Text + "(" + inline.Section + ")"
216 case InlineFlag:
217 return "-" + inline.Text
218 default:
219 return ""
220 }
221}