1package main
2
3import (
4 "fmt"
5 "io"
6 "strings"
7)
8
9type MdocRenderer struct {
10 page *Page
11 w io.Writer
12}
13
14func NewMdocRenderer(page *Page, w io.Writer) *MdocRenderer {
15 return &MdocRenderer{page: page, w: w}
16}
17
18func (r *MdocRenderer) Render() error {
19 if err := r.page.validate(); err != nil { return err; }
20
21 fmt.Fprintf(r.w, ".Dd %s\n", troffEscape(r.page.Date))
22 fmt.Fprintf(r.w, ".Dt %s %d\n", strings.ToUpper(r.page.Name), r.page.Section)
23 fmt.Fprintf(r.w, ".Os %s\n", "aruu")
24
25 r.renderName()
26 if len(r.page.Synopsis) != 0 { r.renderSynopsis(); }
27 if len(r.page.Description) != 0 { r.renderDescription(); }
28 if len(r.page.Options) != 0 { r.renderOptions(); }
29
30 for _, section := range r.page.Sections {
31 r.renderSection(section.Title, section.Blocks)
32 }
33
34 return nil
35}
36
37func (r *MdocRenderer) renderName() {
38 fmt.Fprintln(r.w, ".Sh NAME")
39 fmt.Fprintf(r.w, ".Nm %s\n", troffEscape(r.page.Name))
40 fmt.Fprintf(r.w, ".Nd %s\n", renderInlineSeq(parseInlines(r.page.Summary)))
41}
42
43func (r *MdocRenderer) renderDescription() {
44 fmt.Fprintln(r.w, ".Sh DESCRIPTION")
45 for _, block := range r.page.Description {
46 r.renderBlock(block)
47 }
48}
49
50func (r *MdocRenderer) renderSynopsis() {
51 fmt.Fprintln(r.w, ".Sh SYNOPSIS")
52 for _, form := range r.page.Synopsis {
53 // synopsis macros are emitted one phrase per line
54 fmt.Fprintf(r.w, ".Nm %s\n", troffEscape(r.page.Name))
55 for i := 0; i < len(form.Items); {
56 item := form.Items[i]
57 if item.Kind == SynOptional {
58 fmt.Fprintf(r.w, ".Op %s\n", renderSynopsisPhrase(item.Children, false))
59 i++
60 continue
61 }
62 if item.Kind == SynRequiredGroup {
63 fmt.Fprintf(r.w, ".Brq %s\n", renderSynopsisPhrase(item.Children, false))
64 i++
65 continue
66 }
67
68 phrase, next := consumeSynopsisPhrase(form.Items, i)
69 fmt.Fprintln(r.w, renderSynopsisPhrase(phrase, true))
70 i = next
71 }
72 }
73}
74
75func consumeSynopsisPhrase(items []SynopsisItem, start int) ([]SynopsisItem, int) {
76 phrase := []SynopsisItem{items[start]}
77 i := start + 1
78
79 if items[start].Kind == SynFlag && i < len(items) && items[i].Kind == SynArg {
80 phrase = append(phrase, items[i])
81 i++
82 }
83
84 for i+1 < len(items) && items[i].Kind == SynPipe {
85 phrase = append(phrase, items[i], items[i+1])
86 i += 2
87 }
88
89 return phrase, i
90}
91
92func renderSynopsisPhrase(items []SynopsisItem, topLevel bool) string {
93 var parts []string
94 for _, item := range items {
95 switch item.Kind {
96 case SynFlag:
97 parts = append(parts, renderMacro("Fl", troffEscape(strings.TrimPrefix(item.Text, "-")), topLevel && len(parts) == 0))
98 case SynArg:
99 parts = append(parts, renderMacro("Ar", troffEscape(strings.Trim(item.Text, "<>")), topLevel && len(parts) == 0))
100 case SynCommand:
101 parts = append(parts, renderMacro("Cm", troffEscape(item.Text), topLevel && len(parts) == 0))
102 case SynPipe:
103 parts = append(parts, "|")
104 case SynLiteral:
105 parts = append(parts, troffEscape(item.Text))
106 case SynOptional:
107 parts = append(parts, renderMacro("Op", renderSynopsisPhrase(item.Children, false), topLevel && len(parts) == 0))
108 case SynRequiredGroup:
109 parts = append(parts, renderMacro("Brq", renderSynopsisPhrase(item.Children, false), topLevel && len(parts) == 0))
110 }
111 }
112 return strings.Join(parts, " ")
113}
114
115func renderMacro(name, body string, topLevel bool) string {
116 if body == "" {
117 if topLevel { return "." + name; }
118
119 return name
120 }
121 if topLevel { return "." + name + " " + body; }
122
123 return name + " " + body
124}
125
126func (r *MdocRenderer) renderOptions() {
127 fmt.Fprintln(r.w, ".Sh OPTIONS")
128 fmt.Fprintln(r.w, ".Bl -tag -width Ds")
129 for _, opt := range r.page.Options {
130 fmt.Fprintf(r.w, ".It %s\n", renderSynopsisPhrase(opt.Spec, false))
131 for _, block := range opt.Body {
132 r.renderBlock(block)
133 }
134 }
135 fmt.Fprintln(r.w, ".El")
136}
137
138func (r *MdocRenderer) renderSection(title string, blocks []Block) {
139 fmt.Fprintf(r.w, ".Sh %s\n", troffEscape(title))
140 for _, block := range blocks {
141 r.renderBlock(block)
142 }
143}
144
145func (r *MdocRenderer) renderBlock(block Block) {
146 switch block.Kind {
147 case BlockParagraph:
148 fmt.Fprintln(r.w, ".Pp")
149 fmt.Fprintf(r.w, ".No %s\n", renderInlineSeq(block.Inlines))
150 case BlockTaggedList:
151 fmt.Fprintln(r.w, ".Bl -tag -width Ds")
152 for _, item := range block.Items {
153 fmt.Fprintf(r.w, ".It %s\n", renderInlineSeq(item.Label))
154 if len(item.Body) != 0 {
155 fmt.Fprintln(r.w, ".Pp")
156 fmt.Fprintf(r.w, ".No %s\n", renderInlineSeq(item.Body))
157 }
158 }
159 fmt.Fprintln(r.w, ".El")
160 case BlockSeeAlso:
161 for i, ref := range block.Refs {
162 line := fmt.Sprintf(".Xr %s %s", troffEscape(ref.Name), troffEscape(ref.Section))
163 if i != len(block.Refs)-1 {
164 line += " ,"
165 }
166 fmt.Fprintln(r.w, line)
167 }
168 case BlockSubsection:
169 fmt.Fprintf(r.w, ".Ss %s\n", troffEscape(block.Title))
170 for _, child := range block.Blocks {
171 r.renderBlock(child)
172 }
173 }
174}
175
176func troffEscape(s string) string {
177 s = strings.ReplaceAll(s, "\\", "\\\\")
178 s = strings.ReplaceAll(s, "-", "\\-")
179 if len(s) > 0 && s[0] == '.' {
180 s = "\\&" + s
181 }
182 return s
183}
184
185func renderInlineSeq(inlines []Inline) string {
186 var b strings.Builder
187 // adjacent semantic nodes sometimes need to join without an intervening
188 // space, like some mixed path/emphasis/path forms in FILES
189 for i, inline := range inlines {
190 if i > 0 && needsNoSpace(inlines[i-1], inline) {
191 b.WriteString(" Ns ")
192 }
193 b.WriteString(renderInline(inline))
194 }
195 return b.String()
196}
197
198func renderInline(inline Inline) string {
199 switch inline.Kind {
200 case InlineText:
201 return troffEscape(inline.Text)
202 case InlineNm:
203 return renderMacro("Nm", "", false)
204 case InlineEmph:
205 return renderMacro("Em", renderInlineSeq(inline.Children), false)
206 case InlineLiteral:
207 return renderMacro("Ql", troffEscape(inline.Text), false)
208 case InlinePath:
209 return renderMacro("Pa", troffEscape(inline.Text), false)
210 case InlineXRef:
211 return renderMacro("Xr", troffEscape(inline.Text)+" "+troffEscape(inline.Section), false)
212 case InlineFlag:
213 return renderMacro("Fl", troffEscape(inline.Text), false)
214 default:
215 return ""
216 }
217}
218
219func needsNoSpace(prev, curr Inline) bool {
220 if trailingSpace(prev) || leadingSpace(curr) {
221 return false
222 }
223 return true
224}
225
226func leadingSpace(inline Inline) bool {
227 switch inline.Kind {
228 case InlineText:
229 return len(inline.Text) != 0 && isSpaceByte(inline.Text[0])
230 case InlineEmph:
231 return len(inline.Children) != 0 && leadingSpace(inline.Children[0])
232 default:
233 return false
234 }
235}
236
237func trailingSpace(inline Inline) bool {
238 switch inline.Kind {
239 case InlineText:
240 return len(inline.Text) != 0 && isSpaceByte(inline.Text[len(inline.Text)-1])
241 case InlineEmph:
242 return len(inline.Children) != 0 && trailingSpace(inline.Children[len(inline.Children)-1])
243 default:
244 return false
245 }
246}
247
248func isSpaceByte(b byte) bool { return b == ' ' || b == '\t' || b == '\n'; }