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