Source file src/go/doc/comment/markdown.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  // An mdPrinter holds the state needed for printing a Doc as Markdown.
    14  type mdPrinter struct {
    15  	*Printer
    16  	headingPrefix string
    17  	raw           bytes.Buffer
    18  }
    19  
    20  // Markdown returns a Markdown formatting of the Doc.
    21  // See the [Printer] documentation for ways to customize the Markdown output.
    22  func (p *Printer) Markdown(d *Doc) []byte {
    23  	mp := &mdPrinter{
    24  		Printer:       p,
    25  		headingPrefix: strings.Repeat("#", p.headingLevel()) + " ",
    26  	}
    27  
    28  	var out bytes.Buffer
    29  	for i, x := range d.Content {
    30  		if i > 0 {
    31  			out.WriteByte('\n')
    32  		}
    33  		mp.block(&out, x)
    34  	}
    35  	return out.Bytes()
    36  }
    37  
    38  // block prints the block x to out.
    39  func (p *mdPrinter) block(out *bytes.Buffer, x Block) {
    40  	switch x := x.(type) {
    41  	default:
    42  		fmt.Fprintf(out, "?%T", x)
    43  
    44  	case *Paragraph:
    45  		p.text(out, x.Text)
    46  		out.WriteString("\n")
    47  
    48  	case *Heading:
    49  		out.WriteString(p.headingPrefix)
    50  		p.text(out, x.Text)
    51  		if id := p.headingID(x); id != "" {
    52  			out.WriteString(" {#")
    53  			out.WriteString(id)
    54  			out.WriteString("}")
    55  		}
    56  		out.WriteString("\n")
    57  
    58  	case *Code:
    59  		md := x.Text
    60  		for md != "" {
    61  			var line string
    62  			line, md, _ = strings.Cut(md, "\n")
    63  			if line != "" {
    64  				out.WriteString("\t")
    65  				out.WriteString(line)
    66  			}
    67  			out.WriteString("\n")
    68  		}
    69  
    70  	case *List:
    71  		loose := x.BlankBetween()
    72  		for i, item := range x.Items {
    73  			if i > 0 && loose {
    74  				out.WriteString("\n")
    75  			}
    76  			if n := item.Number; n != "" {
    77  				out.WriteString(" ")
    78  				out.WriteString(n)
    79  				out.WriteString(". ")
    80  			} else {
    81  				out.WriteString("  - ") // SP SP - SP
    82  			}
    83  			for i, blk := range item.Content {
    84  				const fourSpace = "    "
    85  				if i > 0 {
    86  					out.WriteString("\n" + fourSpace)
    87  				}
    88  				p.text(out, blk.(*Paragraph).Text)
    89  				out.WriteString("\n")
    90  			}
    91  		}
    92  	}
    93  }
    94  
    95  // text prints the text sequence x to out.
    96  func (p *mdPrinter) text(out *bytes.Buffer, x []Text) {
    97  	p.raw.Reset()
    98  	p.rawText(&p.raw, x)
    99  	line := bytes.TrimSpace(p.raw.Bytes())
   100  	if len(line) == 0 {
   101  		return
   102  	}
   103  	switch line[0] {
   104  	case '+', '-', '*', '#':
   105  		// Escape what would be the start of an unordered list or heading.
   106  		out.WriteByte('\\')
   107  	case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
   108  		i := 1
   109  		for i < len(line) && '0' <= line[i] && line[i] <= '9' {
   110  			i++
   111  		}
   112  		if i < len(line) && (line[i] == '.' || line[i] == ')') {
   113  			// Escape what would be the start of an ordered list.
   114  			out.Write(line[:i])
   115  			out.WriteByte('\\')
   116  			line = line[i:]
   117  		}
   118  	}
   119  	out.Write(line)
   120  }
   121  
   122  // rawText prints the text sequence x to out,
   123  // without worrying about escaping characters
   124  // that have special meaning at the start of a Markdown line.
   125  func (p *mdPrinter) rawText(out *bytes.Buffer, x []Text) {
   126  	for _, t := range x {
   127  		switch t := t.(type) {
   128  		case Plain:
   129  			p.escape(out, string(t))
   130  		case Italic:
   131  			out.WriteString("*")
   132  			p.escape(out, string(t))
   133  			out.WriteString("*")
   134  		case *Link:
   135  			out.WriteString("[")
   136  			p.rawText(out, t.Text)
   137  			out.WriteString("](")
   138  			out.WriteString(t.URL)
   139  			out.WriteString(")")
   140  		case *DocLink:
   141  			url := p.docLinkURL(t)
   142  			if url != "" {
   143  				out.WriteString("[")
   144  			}
   145  			p.rawText(out, t.Text)
   146  			if url != "" {
   147  				out.WriteString("](")
   148  				url = strings.ReplaceAll(url, "(", "%28")
   149  				url = strings.ReplaceAll(url, ")", "%29")
   150  				out.WriteString(url)
   151  				out.WriteString(")")
   152  			}
   153  		}
   154  	}
   155  }
   156  
   157  // escape prints s to out as plain text,
   158  // escaping special characters to avoid being misinterpreted
   159  // as Markdown markup sequences.
   160  func (p *mdPrinter) escape(out *bytes.Buffer, s string) {
   161  	start := 0
   162  	for i := 0; i < len(s); i++ {
   163  		switch s[i] {
   164  		case '\n':
   165  			// Turn all \n into spaces, for a few reasons:
   166  			//   - Avoid introducing paragraph breaks accidentally.
   167  			//   - Avoid the need to reindent after the newline.
   168  			//   - Avoid problems with Markdown renderers treating
   169  			//     every mid-paragraph newline as a <br>.
   170  			out.WriteString(s[start:i])
   171  			out.WriteByte(' ')
   172  			start = i + 1
   173  			continue
   174  		case '`', '_', '*', '[', '<', '\\':
   175  			// Not all of these need to be escaped all the time,
   176  			// but is valid and easy to do so.
   177  			// We assume the Markdown is being passed to a
   178  			// Markdown renderer, not edited by a person,
   179  			// so it's fine to have escapes that are not strictly
   180  			// necessary in some cases.
   181  			out.WriteString(s[start:i])
   182  			out.WriteByte('\\')
   183  			out.WriteByte(s[i])
   184  			start = i + 1
   185  		}
   186  	}
   187  	out.WriteString(s[start:])
   188  }
   189  

View as plain text