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