go/doc/comment: parse and print code

[This CL is part of a sequence implementing the proposal #51082.
The design doc is at https://go.dev/s/godocfmt-design.]

Implement indented code blocks.

For #51082.

Change-Id: I0eacbf56e101424a875386cb6f26174b239561f5
Reviewed-on: https://go-review.googlesource.com/c/go/+/397285
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Russ Cox 2022-04-03 16:40:03 -04:00
parent 6eceabf119
commit 3f5d099663
9 changed files with 263 additions and 9 deletions

View File

@ -51,6 +51,11 @@ func (p *htmlPrinter) block(out *bytes.Buffer, x Block) {
out.WriteString("</h")
out.WriteString(h)
out.WriteString(">\n")
case *Code:
out.WriteString("<pre>")
p.escape(out, x.Text)
out.WriteString("</pre>\n")
}
}

View File

@ -54,6 +54,18 @@ func (p *mdPrinter) block(out *bytes.Buffer, x Block) {
out.WriteString("}")
}
out.WriteString("\n")
case *Code:
md := x.Text
for md != "" {
var line string
line, md, _ = strings.Cut(md, "\n")
if line != "" {
out.WriteString("\t")
out.WriteString(line)
}
out.WriteString("\n")
}
}
}

View File

@ -309,6 +309,9 @@ func (p *Parser) Parse(text string) *Doc {
case line == "":
// emit nothing
case isIndented(line):
b, lines = d.code(lines)
case (len(lines) == 1 || lines[1] == "") && !didHeading && isOldHeading(line, all, len(all)-n):
b = d.oldHeading(line)
didHeading = true
@ -473,17 +476,51 @@ func (d *parseDoc) heading(line string) Block {
return &Heading{Text: []Text{Plain(strings.TrimSpace(line[1:]))}}
}
// code returns a code block built from the indented text
// at the start of lines, along with the remainder of the lines.
// If there is no indented text at the start, or if the indented
// text consists only of empty lines, code returns a nil Block.
func (d *parseDoc) code(lines []string) (b Block, rest []string) {
lines, rest = indented(lines)
body := unindent(lines)
if len(body) == 0 {
return nil, rest
}
body = append(body, "") // to get final \n from Join
return &Code{Text: strings.Join(body, "\n")}, rest
}
// isIndented reports whether the line is indented,
// meaning it starts with a space or tab.
func isIndented(line string) bool {
return line != "" && (line[0] == ' ' || line[0] == '\t')
}
// indented splits lines into an initial indented section
// and the remaining lines, returning the two halves.
func indented(lines []string) (indented, rest []string) {
// Blank lines mid-run are OK, but not at the end.
i := 0
for i < len(lines) && (isIndented(lines[i]) || lines[i] == "") {
i++
}
for i > 0 && lines[i-1] == "" {
i--
}
return lines[:i], lines[i:]
}
// paragraph returns a paragraph block built from the
// unindented text at the start of lines, along with the remainder of the lines.
// If there is no unindented text at the start of lines,
// then paragraph returns a nil Block.
func (d *parseDoc) paragraph(lines []string) (b Block, rest []string) {
// TODO: Paragraph should be interrupted by any indented line,
// Paragraph is interrupted by any indented line,
// which is either a list or a code block,
// and of course by a blank line.
// It should not be interrupted by a # line - headings must stand alone.
// It is not interrupted by a # line - headings must stand alone.
i := 0
for i < len(lines) && lines[i] != "" {
for i < len(lines) && lines[i] != "" && !isIndented(lines[i]) {
i++
}
lines, rest = lines[:i], lines[i:]

View File

@ -213,6 +213,18 @@ func (p *commentPrinter) block(out *bytes.Buffer, x Block) {
out.WriteString("# ")
p.text(out, "", x.Text)
out.WriteString("\n")
case *Code:
md := x.Text
for md != "" {
var line string
line, md, _ = strings.Cut(md, "\n")
if line != "" {
out.WriteString("\t")
out.WriteString(line)
}
out.WriteString("\n")
}
}
}

94
src/go/doc/comment/testdata/code.txt vendored Normal file
View File

@ -0,0 +1,94 @@
-- input --
Text.
A tab-indented
(no, not eight-space indented)
code block and haiku.
More text.
One space
is
enough
to
start
a
block.
More text.
Blocks
can
have
blank
lines.
-- gofmt --
Text.
A tab-indented
(no, not eight-space indented)
code block and haiku.
More text.
One space
is
enough
to
start
a
block.
More text.
Blocks
can
have
blank
lines.
-- markdown --
Text.
A tab-indented
(no, not eight-space indented)
code block and haiku.
More text.
One space
is
enough
to
start
a
block.
More text.
Blocks
can
have
blank
lines.
-- html --
<p>Text.
<pre>A tab-indented
(no, not eight-space indented)
code block and haiku.
</pre>
<p>More text.
<pre>One space
is
enough
to
start
a
block.
</pre>
<p>More text.
<pre> Blocks
can
have
blank
lines.
</pre>

31
src/go/doc/comment/testdata/code2.txt vendored Normal file
View File

@ -0,0 +1,31 @@
-- input --
Text.
A tab-indented
(no, not eight-space indented)
code block and haiku.
More text.
-- gofmt --
Text.
A tab-indented
(no, not eight-space indented)
code block and haiku.
More text.
-- markdown --
Text.
A tab-indented
(no, not eight-space indented)
code block and haiku.
More text.
-- html --
<p>Text.
<pre>A tab-indented
(no, not eight-space indented)
code block and haiku.
</pre>
<p>More text.

33
src/go/doc/comment/testdata/code3.txt vendored Normal file
View File

@ -0,0 +1,33 @@
-- input --
Text.
$
A tab-indented
(surrounded by more blank lines)
code block and haiku.
$
More text.
-- gofmt --
Text.
A tab-indented
(surrounded by more blank lines)
code block and haiku.
More text.
-- markdown --
Text.
A tab-indented
(surrounded by more blank lines)
code block and haiku.
More text.
-- html --
<p>Text.
<pre>A tab-indented
(surrounded by more blank lines)
code block and haiku.
</pre>
<p>More text.

12
src/go/doc/comment/testdata/text9.txt vendored Normal file
View File

@ -0,0 +1,12 @@
{"TextPrefix":"|", "TextCodePrefix": "@"}
-- input --
Hello, world
Code block here.
-- gofmt --
Hello, world
Code block here.
-- text --
|Hello, world
|
@Code block here.

View File

@ -15,18 +15,23 @@ import (
// A textPrinter holds the state needed for printing a Doc as plain text.
type textPrinter struct {
*Printer
long strings.Builder
prefix string
width int
long strings.Builder
prefix string
codePrefix string
width int
}
// Text returns a textual formatting of the Doc.
// See the [Printer] documentation for ways to customize the text output.
func (p *Printer) Text(d *Doc) []byte {
tp := &textPrinter{
Printer: p,
prefix: p.TextPrefix,
width: p.TextWidth,
Printer: p,
prefix: p.TextPrefix,
codePrefix: p.TextCodePrefix,
width: p.TextWidth,
}
if tp.codePrefix == "" {
tp.codePrefix = p.TextPrefix + "\t"
}
if tp.width == 0 {
tp.width = 80 - utf8.RuneCountInString(tp.prefix)
@ -35,6 +40,7 @@ func (p *Printer) Text(d *Doc) []byte {
var out bytes.Buffer
for i, x := range d.Content {
if i > 0 && blankBefore(x) {
out.WriteString(tp.prefix)
writeNL(&out)
}
tp.block(&out, x)
@ -86,6 +92,18 @@ func (p *textPrinter) block(out *bytes.Buffer, x Block) {
out.WriteString(p.prefix)
out.WriteString("# ")
p.text(out, x.Text)
case *Code:
text := x.Text
for text != "" {
var line string
line, text, _ = strings.Cut(text, "\n")
if line != "" {
out.WriteString(p.codePrefix)
out.WriteString(line)
}
writeNL(out)
}
}
}