• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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// An mdPrinter holds the state needed for printing a Doc as Markdown.
14type 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.
22func (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.
39func (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.
96func (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.
125func (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.
160func (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