...

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

Documentation: go/doc/comment

     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  	headingPrefix string
   152  	needDoc       map[string]bool
   153  }
   154  
   155  // Comment returns the standard Go formatting of the Doc,
   156  // without any comment markers.
   157  func (p *Printer) Comment(d *Doc) []byte {
   158  	cp := &commentPrinter{Printer: p}
   159  	var out bytes.Buffer
   160  	for i, x := range d.Content {
   161  		if i > 0 && blankBefore(x) {
   162  			out.WriteString("\n")
   163  		}
   164  		cp.block(&out, x)
   165  	}
   166  
   167  	// Print one block containing all the link definitions that were used,
   168  	// and then a second block containing all the unused ones.
   169  	// This makes it easy to clean up the unused ones: gofmt and
   170  	// delete the final block. And it's a nice visual signal without
   171  	// affecting the way the comment formats for users.
   172  	for i := 0; i < 2; i++ {
   173  		used := i == 0
   174  		first := true
   175  		for _, def := range d.Links {
   176  			if def.Used == used {
   177  				if first {
   178  					out.WriteString("\n")
   179  					first = false
   180  				}
   181  				out.WriteString("[")
   182  				out.WriteString(def.Text)
   183  				out.WriteString("]: ")
   184  				out.WriteString(def.URL)
   185  				out.WriteString("\n")
   186  			}
   187  		}
   188  	}
   189  
   190  	return out.Bytes()
   191  }
   192  
   193  // blankBefore reports whether the block x requires a blank line before it.
   194  // All blocks do, except for Lists that return false from x.BlankBefore().
   195  func blankBefore(x Block) bool {
   196  	if x, ok := x.(*List); ok {
   197  		return x.BlankBefore()
   198  	}
   199  	return true
   200  }
   201  
   202  // block prints the block x to out.
   203  func (p *commentPrinter) block(out *bytes.Buffer, x Block) {
   204  	switch x := x.(type) {
   205  	default:
   206  		fmt.Fprintf(out, "?%T", x)
   207  
   208  	case *Paragraph:
   209  		p.text(out, "", x.Text)
   210  		out.WriteString("\n")
   211  
   212  	case *Heading:
   213  		out.WriteString("# ")
   214  		p.text(out, "", x.Text)
   215  		out.WriteString("\n")
   216  
   217  	case *Code:
   218  		md := x.Text
   219  		for md != "" {
   220  			var line string
   221  			line, md, _ = strings.Cut(md, "\n")
   222  			if line != "" {
   223  				out.WriteString("\t")
   224  				out.WriteString(line)
   225  			}
   226  			out.WriteString("\n")
   227  		}
   228  
   229  	case *List:
   230  		loose := x.BlankBetween()
   231  		for i, item := range x.Items {
   232  			if i > 0 && loose {
   233  				out.WriteString("\n")
   234  			}
   235  			out.WriteString(" ")
   236  			if item.Number == "" {
   237  				out.WriteString(" - ")
   238  			} else {
   239  				out.WriteString(item.Number)
   240  				out.WriteString(". ")
   241  			}
   242  			for i, blk := range item.Content {
   243  				const fourSpace = "    "
   244  				if i > 0 {
   245  					out.WriteString("\n" + fourSpace)
   246  				}
   247  				p.text(out, fourSpace, blk.(*Paragraph).Text)
   248  				out.WriteString("\n")
   249  			}
   250  		}
   251  	}
   252  }
   253  
   254  // text prints the text sequence x to out.
   255  func (p *commentPrinter) text(out *bytes.Buffer, indent string, x []Text) {
   256  	for _, t := range x {
   257  		switch t := t.(type) {
   258  		case Plain:
   259  			p.indent(out, indent, string(t))
   260  		case Italic:
   261  			p.indent(out, indent, string(t))
   262  		case *Link:
   263  			if t.Auto {
   264  				p.text(out, indent, t.Text)
   265  			} else {
   266  				out.WriteString("[")
   267  				p.text(out, indent, t.Text)
   268  				out.WriteString("]")
   269  			}
   270  		case *DocLink:
   271  			out.WriteString("[")
   272  			p.text(out, indent, t.Text)
   273  			out.WriteString("]")
   274  		}
   275  	}
   276  }
   277  
   278  // indent prints s to out, indenting with the indent string
   279  // after each newline in s.
   280  func (p *commentPrinter) indent(out *bytes.Buffer, indent, s string) {
   281  	for s != "" {
   282  		line, rest, ok := strings.Cut(s, "\n")
   283  		out.WriteString(line)
   284  		if ok {
   285  			out.WriteString("\n")
   286  			out.WriteString(indent)
   287  		}
   288  		s = rest
   289  	}
   290  }
   291  

View as plain text