master xplshn/aruu / scripts / mkman / txt.go
  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}