1// Copyright 2022 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package comment
6
7import (
8	"bytes"
9	"fmt"
10	"strings"
11)
12
13// A Printer is a doc comment printer.
14// The fields in the struct can be filled in before calling
15// any of the printing methods
16// in order to customize the details of the printing process.
17type Printer struct {
18	// HeadingLevel is the nesting level used for
19	// HTML and Markdown headings.
20	// If HeadingLevel is zero, it defaults to level 3,
21	// meaning to use <h3> and ###.
22	HeadingLevel int
23
24	// HeadingID is a function that computes the heading ID
25	// (anchor tag) to use for the heading h when generating
26	// HTML and Markdown. If HeadingID returns an empty string,
27	// then the heading ID is omitted.
28	// If HeadingID is nil, h.DefaultID is used.
29	HeadingID func(h *Heading) string
30
31	// DocLinkURL is a function that computes the URL for the given DocLink.
32	// If DocLinkURL is nil, then link.DefaultURL(p.DocLinkBaseURL) is used.
33	DocLinkURL func(link *DocLink) string
34
35	// DocLinkBaseURL is used when DocLinkURL is nil,
36	// passed to [DocLink.DefaultURL] to construct a DocLink's URL.
37	// See that method's documentation for details.
38	DocLinkBaseURL string
39
40	// TextPrefix is a prefix to print at the start of every line
41	// when generating text output using the Text method.
42	TextPrefix string
43
44	// TextCodePrefix is the prefix to print at the start of each
45	// preformatted (code block) line when generating text output,
46	// instead of (not in addition to) TextPrefix.
47	// If TextCodePrefix is the empty string, it defaults to TextPrefix+"\t".
48	TextCodePrefix string
49
50	// TextWidth is the maximum width text line to generate,
51	// measured in Unicode code points,
52	// excluding TextPrefix and the newline character.
53	// If TextWidth is zero, it defaults to 80 minus the number of code points in TextPrefix.
54	// If TextWidth is negative, there is no limit.
55	TextWidth int
56}
57
58func (p *Printer) headingLevel() int {
59	if p.HeadingLevel <= 0 {
60		return 3
61	}
62	return p.HeadingLevel
63}
64
65func (p *Printer) headingID(h *Heading) string {
66	if p.HeadingID == nil {
67		return h.DefaultID()
68	}
69	return p.HeadingID(h)
70}
71
72func (p *Printer) docLinkURL(link *DocLink) string {
73	if p.DocLinkURL != nil {
74		return p.DocLinkURL(link)
75	}
76	return link.DefaultURL(p.DocLinkBaseURL)
77}
78
79// DefaultURL constructs and returns the documentation URL for l,
80// using baseURL as a prefix for links to other packages.
81//
82// The possible forms returned by DefaultURL are:
83//   - baseURL/ImportPath, for a link to another package
84//   - baseURL/ImportPath#Name, for a link to a const, func, type, or var in another package
85//   - baseURL/ImportPath#Recv.Name, for a link to a method in another package
86//   - #Name, for a link to a const, func, type, or var in this package
87//   - #Recv.Name, for a link to a method in this package
88//
89// If baseURL ends in a trailing slash, then DefaultURL inserts
90// a slash between ImportPath and # in the anchored forms.
91// For example, here are some baseURL values and URLs they can generate:
92//
93//	"/pkg/" → "/pkg/math/#Sqrt"
94//	"/pkg"  → "/pkg/math#Sqrt"
95//	"/"     → "/math/#Sqrt"
96//	""      → "/math#Sqrt"
97func (l *DocLink) DefaultURL(baseURL string) string {
98	if l.ImportPath != "" {
99		slash := ""
100		if strings.HasSuffix(baseURL, "/") {
101			slash = "/"
102		} else {
103			baseURL += "/"
104		}
105		switch {
106		case l.Name == "":
107			return baseURL + l.ImportPath + slash
108		case l.Recv != "":
109			return baseURL + l.ImportPath + slash + "#" + l.Recv + "." + l.Name
110		default:
111			return baseURL + l.ImportPath + slash + "#" + l.Name
112		}
113	}
114	if l.Recv != "" {
115		return "#" + l.Recv + "." + l.Name
116	}
117	return "#" + l.Name
118}
119
120// DefaultID returns the default anchor ID for the heading h.
121//
122// The default anchor ID is constructed by converting every
123// rune that is not alphanumeric ASCII to an underscore
124// and then adding the prefix “hdr-”.
125// For example, if the heading text is “Go Doc Comments”,
126// the default ID is “hdr-Go_Doc_Comments”.
127func (h *Heading) DefaultID() string {
128	// Note: The “hdr-” prefix is important to avoid DOM clobbering attacks.
129	// See https://pkg.go.dev/github.com/google/safehtml#Identifier.
130	var out strings.Builder
131	var p textPrinter
132	p.oneLongLine(&out, h.Text)
133	s := strings.TrimSpace(out.String())
134	if s == "" {
135		return ""
136	}
137	out.Reset()
138	out.WriteString("hdr-")
139	for _, r := range s {
140		if r < 0x80 && isIdentASCII(byte(r)) {
141			out.WriteByte(byte(r))
142		} else {
143			out.WriteByte('_')
144		}
145	}
146	return out.String()
147}
148
149type commentPrinter struct {
150	*Printer
151}
152
153// Comment returns the standard Go formatting of the [Doc],
154// without any comment markers.
155func (p *Printer) Comment(d *Doc) []byte {
156	cp := &commentPrinter{Printer: p}
157	var out bytes.Buffer
158	for i, x := range d.Content {
159		if i > 0 && blankBefore(x) {
160			out.WriteString("\n")
161		}
162		cp.block(&out, x)
163	}
164
165	// Print one block containing all the link definitions that were used,
166	// and then a second block containing all the unused ones.
167	// This makes it easy to clean up the unused ones: gofmt and
168	// delete the final block. And it's a nice visual signal without
169	// affecting the way the comment formats for users.
170	for i := 0; i < 2; i++ {
171		used := i == 0
172		first := true
173		for _, def := range d.Links {
174			if def.Used == used {
175				if first {
176					out.WriteString("\n")
177					first = false
178				}
179				out.WriteString("[")
180				out.WriteString(def.Text)
181				out.WriteString("]: ")
182				out.WriteString(def.URL)
183				out.WriteString("\n")
184			}
185		}
186	}
187
188	return out.Bytes()
189}
190
191// blankBefore reports whether the block x requires a blank line before it.
192// All blocks do, except for Lists that return false from x.BlankBefore().
193func blankBefore(x Block) bool {
194	if x, ok := x.(*List); ok {
195		return x.BlankBefore()
196	}
197	return true
198}
199
200// block prints the block x to out.
201func (p *commentPrinter) block(out *bytes.Buffer, x Block) {
202	switch x := x.(type) {
203	default:
204		fmt.Fprintf(out, "?%T", x)
205
206	case *Paragraph:
207		p.text(out, "", x.Text)
208		out.WriteString("\n")
209
210	case *Heading:
211		out.WriteString("# ")
212		p.text(out, "", x.Text)
213		out.WriteString("\n")
214
215	case *Code:
216		md := x.Text
217		for md != "" {
218			var line string
219			line, md, _ = strings.Cut(md, "\n")
220			if line != "" {
221				out.WriteString("\t")
222				out.WriteString(line)
223			}
224			out.WriteString("\n")
225		}
226
227	case *List:
228		loose := x.BlankBetween()
229		for i, item := range x.Items {
230			if i > 0 && loose {
231				out.WriteString("\n")
232			}
233			out.WriteString(" ")
234			if item.Number == "" {
235				out.WriteString(" - ")
236			} else {
237				out.WriteString(item.Number)
238				out.WriteString(". ")
239			}
240			for i, blk := range item.Content {
241				const fourSpace = "    "
242				if i > 0 {
243					out.WriteString("\n" + fourSpace)
244				}
245				p.text(out, fourSpace, blk.(*Paragraph).Text)
246				out.WriteString("\n")
247			}
248		}
249	}
250}
251
252// text prints the text sequence x to out.
253func (p *commentPrinter) text(out *bytes.Buffer, indent string, x []Text) {
254	for _, t := range x {
255		switch t := t.(type) {
256		case Plain:
257			p.indent(out, indent, string(t))
258		case Italic:
259			p.indent(out, indent, string(t))
260		case *Link:
261			if t.Auto {
262				p.text(out, indent, t.Text)
263			} else {
264				out.WriteString("[")
265				p.text(out, indent, t.Text)
266				out.WriteString("]")
267			}
268		case *DocLink:
269			out.WriteString("[")
270			p.text(out, indent, t.Text)
271			out.WriteString("]")
272		}
273	}
274}
275
276// indent prints s to out, indenting with the indent string
277// after each newline in s.
278func (p *commentPrinter) indent(out *bytes.Buffer, indent, s string) {
279	for s != "" {
280		line, rest, ok := strings.Cut(s, "\n")
281		out.WriteString(line)
282		if ok {
283			out.WriteString("\n")
284			out.WriteString(indent)
285		}
286		s = rest
287	}
288}
289