Source file src/go/doc/comment/print.go

     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  
     5  package comment
     6  
     7  import (
     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.
    17  type 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  
    58  func (p *Printer) headingLevel() int {
    59  	if p.HeadingLevel <= 0 {
    60  		return 3
    61  	}
    62  	return p.HeadingLevel
    63  }
    64  
    65  func (p *Printer) headingID(h *Heading) string {
    66  	if p.HeadingID == nil {
    67  		return h.DefaultID()
    68  	}
    69  	return p.HeadingID(h)
    70  }
    71  
    72  func (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"
    97  func (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”.
   127  func (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  
   149  type commentPrinter struct {
   150  	*Printer
   151  }
   152  
   153  // Comment returns the standard Go formatting of the [Doc],
   154  // without any comment markers.
   155  func (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().
   193  func 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.
   201  func (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.
   253  func (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.
   278  func (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  

View as plain text