]> Cypherpunks repositories - gostls13.git/commitdiff
go/doc/comment: parse and print headings
authorRuss Cox <rsc@golang.org>
Sun, 3 Apr 2022 20:30:08 +0000 (16:30 -0400)
committerRuss Cox <rsc@golang.org>
Mon, 11 Apr 2022 16:31:46 +0000 (16:31 +0000)
[This CL is part of a sequence implementing the proposal #51082.
The design doc is at https://go.dev/s/godocfmt-design.]

Implement both old-style and new-style headings, like:

Text here.

Old Style Heading

More text here.

# New Style Heading

More text here.

For #51082.

Change-Id: I0d735782d0d345794fc2d4e1bdaa0251b8d4bba2
Reviewed-on: https://go-review.googlesource.com/c/go/+/397284
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
api/next/51082.txt
src/go/doc/comment/html.go
src/go/doc/comment/markdown.go
src/go/doc/comment/parse.go
src/go/doc/comment/print.go
src/go/doc/comment/testdata/head.txt [new file with mode: 0644]
src/go/doc/comment/testdata/head2.txt [new file with mode: 0644]
src/go/doc/comment/testdata/head3.txt [new file with mode: 0644]
src/go/doc/comment/text.go

index 0e5cbc5880ac2807765a321eb815617cfb9cfb1c..72c5b2e246ef194a30d8ce1bd066f7e22d41816e 100644 (file)
@@ -1,5 +1,6 @@
 pkg go/doc/comment, func DefaultLookupPackage(string) (string, bool) #51082
 pkg go/doc/comment, method (*DocLink) DefaultURL(string) string #51082
+pkg go/doc/comment, method (*Heading) DefaultID() string #51082
 pkg go/doc/comment, method (*List) BlankBefore() bool #51082
 pkg go/doc/comment, method (*List) BlankBetween() bool #51082
 pkg go/doc/comment, method (*Parser) Parse(string) *Doc #51082
index da2300d12821b62622c3e1a33695c9c26c99cd6a..f6ea588b3d5574ccc87d4154eca526a41737650c 100644 (file)
@@ -7,6 +7,7 @@ package comment
 import (
        "bytes"
        "fmt"
+       "strconv"
 )
 
 // An htmlPrinter holds the state needed for printing a Doc as HTML.
@@ -35,6 +36,21 @@ func (p *htmlPrinter) block(out *bytes.Buffer, x Block) {
                out.WriteString("<p>")
                p.text(out, x.Text)
                out.WriteString("\n")
+
+       case *Heading:
+               out.WriteString("<h")
+               h := strconv.Itoa(p.headingLevel())
+               out.WriteString(h)
+               if id := p.headingID(x); id != "" {
+                       out.WriteString(` id="`)
+                       p.escape(out, id)
+                       out.WriteString(`"`)
+               }
+               out.WriteString(">")
+               p.text(out, x.Text)
+               out.WriteString("</h")
+               out.WriteString(h)
+               out.WriteString(">\n")
        }
 }
 
index 309e1805731c3fc6e73570deb570cb9ded8e51f0..44ea727daeeaae5542d30d31fea9e3a30527d939 100644 (file)
@@ -13,13 +13,17 @@ import (
 // An mdPrinter holds the state needed for printing a Doc as Markdown.
 type mdPrinter struct {
        *Printer
-       raw bytes.Buffer
+       headingPrefix string
+       raw           bytes.Buffer
 }
 
 // Markdown returns a Markdown formatting of the Doc.
 // See the [Printer] documentation for ways to customize the Markdown output.
 func (p *Printer) Markdown(d *Doc) []byte {
-       mp := &mdPrinter{Printer: p}
+       mp := &mdPrinter{
+               Printer:       p,
+               headingPrefix: strings.Repeat("#", p.headingLevel()) + " ",
+       }
 
        var out bytes.Buffer
        for i, x := range d.Content {
@@ -40,6 +44,16 @@ func (p *mdPrinter) block(out *bytes.Buffer, x Block) {
        case *Paragraph:
                p.text(out, x.Text)
                out.WriteString("\n")
+
+       case *Heading:
+               out.WriteString(p.headingPrefix)
+               p.text(out, x.Text)
+               if id := p.headingID(x); id != "" {
+                       out.WriteString(" {#")
+                       out.WriteString(id)
+                       out.WriteString("}")
+               }
+               out.WriteString("\n")
        }
 }
 
index 920b446c7e9880940335d7ed024440de20f0c67d..25b5f10f2fcce63fa48747bd0d145223d4323788 100644 (file)
@@ -298,15 +298,34 @@ func (p *Parser) Parse(text string) *Doc {
        // First pass: break into block structure and collect known links.
        // The text is all recorded as Plain for now.
        // TODO: Break into actual block structure.
+       didHeading := false
+       all := lines
        for len(lines) > 0 {
                line := lines[0]
-               if line != "" {
-                       var b Block
+               n := len(lines)
+               var b Block
+
+               switch {
+               case line == "":
+                       // emit nothing
+
+               case (len(lines) == 1 || lines[1] == "") && !didHeading && isOldHeading(line, all, len(all)-n):
+                       b = d.oldHeading(line)
+                       didHeading = true
+
+               case (len(lines) == 1 || lines[1] == "") && isHeading(line):
+                       b = d.heading(line)
+                       didHeading = true
+
+               default:
                        b, lines = d.paragraph(lines)
-                       if b != nil {
-                               d.Content = append(d.Content, b)
-                       }
-               } else {
+                       didHeading = false
+               }
+
+               if b != nil {
+                       d.Content = append(d.Content, b)
+               }
+               if len(lines) == n {
                        lines = lines[1:]
                }
        }
@@ -436,6 +455,24 @@ func isOldHeading(line string, all []string, off int) bool {
        return true
 }
 
+// oldHeading returns the *Heading for the given old-style section heading line.
+func (d *parseDoc) oldHeading(line string) Block {
+       return &Heading{Text: []Text{Plain(strings.TrimSpace(line))}}
+}
+
+// isHeading reports whether line is a new-style section heading.
+func isHeading(line string) bool {
+       return len(line) >= 2 &&
+               line[0] == '#' &&
+               (line[1] == ' ' || line[1] == '\t') &&
+               strings.TrimSpace(line) != "#"
+}
+
+// heading returns the *Heading for the given new-style section heading line.
+func (d *parseDoc) heading(line string) Block {
+       return &Heading{Text: []Text{Plain(strings.TrimSpace(line[1:]))}}
+}
+
 // 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,
index 2ef8d7375deda9fb8412c0c9a7f98cbddc4a1e37..db520e81925e9edbee59582627631a8ed62aebc8 100644 (file)
@@ -55,6 +55,20 @@ type Printer struct {
        TextWidth int
 }
 
+func (p *Printer) headingLevel() int {
+       if p.HeadingLevel <= 0 {
+               return 3
+       }
+       return p.HeadingLevel
+}
+
+func (p *Printer) headingID(h *Heading) string {
+       if p.HeadingID == nil {
+               return h.DefaultID()
+       }
+       return p.HeadingID(h)
+}
+
 func (p *Printer) docLinkURL(link *DocLink) string {
        if p.DocLinkURL != nil {
                return p.DocLinkURL(link)
@@ -103,6 +117,35 @@ func (l *DocLink) DefaultURL(baseURL string) string {
        return "#" + l.Name
 }
 
+// DefaultID returns the default anchor ID for the heading h.
+//
+// The default anchor ID is constructed by converting every
+// rune that is not alphanumeric ASCII to an underscore
+// and then adding the prefix “hdr-”.
+// For example, if the heading text is “Go Doc Comments”,
+// the default ID is “hdr-Go_Doc_Comments”.
+func (h *Heading) DefaultID() string {
+       // Note: The “hdr-” prefix is important to avoid DOM clobbering attacks.
+       // See https://pkg.go.dev/github.com/google/safehtml#Identifier.
+       var out strings.Builder
+       var p textPrinter
+       p.oneLongLine(&out, h.Text)
+       s := strings.TrimSpace(out.String())
+       if s == "" {
+               return ""
+       }
+       out.Reset()
+       out.WriteString("hdr-")
+       for _, r := range s {
+               if r < 0x80 && isIdentASCII(byte(r)) {
+                       out.WriteByte(byte(r))
+               } else {
+                       out.WriteByte('_')
+               }
+       }
+       return out.String()
+}
+
 type commentPrinter struct {
        *Printer
        headingPrefix string
@@ -165,6 +208,11 @@ func (p *commentPrinter) block(out *bytes.Buffer, x Block) {
        case *Paragraph:
                p.text(out, "", x.Text)
                out.WriteString("\n")
+
+       case *Heading:
+               out.WriteString("# ")
+               p.text(out, "", x.Text)
+               out.WriteString("\n")
        }
 }
 
diff --git a/src/go/doc/comment/testdata/head.txt b/src/go/doc/comment/testdata/head.txt
new file mode 100644 (file)
index 0000000..b99a8c5
--- /dev/null
@@ -0,0 +1,92 @@
+-- input --
+Some text.
+
+An Old Heading
+
+Not An Old Heading.
+
+And some text.
+
+# A New Heading.
+
+And some more text.
+
+# Not a heading,
+because text follows it.
+
+Because text precedes it,
+# not a heading.
+
+## Not a heading either.
+
+-- gofmt --
+Some text.
+
+# An Old Heading
+
+Not An Old Heading.
+
+And some text.
+
+# A New Heading.
+
+And some more text.
+
+# Not a heading,
+because text follows it.
+
+Because text precedes it,
+# not a heading.
+
+## Not a heading either.
+
+-- text --
+Some text.
+
+# An Old Heading
+
+Not An Old Heading.
+
+And some text.
+
+# A New Heading.
+
+And some more text.
+
+# Not a heading, because text follows it.
+
+Because text precedes it, # not a heading.
+
+## Not a heading either.
+
+-- markdown --
+Some text.
+
+### An Old Heading {#hdr-An_Old_Heading}
+
+Not An Old Heading.
+
+And some text.
+
+### A New Heading. {#hdr-A_New_Heading_}
+
+And some more text.
+
+\# Not a heading, because text follows it.
+
+Because text precedes it, # not a heading.
+
+\## Not a heading either.
+
+-- html --
+<p>Some text.
+<h3 id="hdr-An_Old_Heading">An Old Heading</h3>
+<p>Not An Old Heading.
+<p>And some text.
+<h3 id="hdr-A_New_Heading_">A New Heading.</h3>
+<p>And some more text.
+<p># Not a heading,
+because text follows it.
+<p>Because text precedes it,
+# not a heading.
+<p>## Not a heading either.
diff --git a/src/go/doc/comment/testdata/head2.txt b/src/go/doc/comment/testdata/head2.txt
new file mode 100644 (file)
index 0000000..d357632
--- /dev/null
@@ -0,0 +1,36 @@
+-- input --
+✦
+
+Almost a+heading
+
+✦
+
+Don't be a heading
+
+✦
+
+A.b is a heading
+
+✦
+
+A. b is not a heading
+
+✦
+-- gofmt --
+✦
+
+Almost a+heading
+
+✦
+
+Don't be a heading
+
+✦
+
+# A.b is a heading
+
+✦
+
+A. b is not a heading
+
+✦
diff --git a/src/go/doc/comment/testdata/head3.txt b/src/go/doc/comment/testdata/head3.txt
new file mode 100644 (file)
index 0000000..dbb7cb3
--- /dev/null
@@ -0,0 +1,7 @@
+{"HeadingLevel": 5}
+-- input --
+# Heading
+-- markdown --
+##### Heading {#hdr-Heading}
+-- html --
+<h5 id="hdr-Heading">Heading</h5>
index d6d651b5d613df929b3a0a013b3fd94d9966b6fe..1eddad30fd51e86a0222052169f89c42cb30f262 100644 (file)
@@ -15,7 +15,7 @@ import (
 // A textPrinter holds the state needed for printing a Doc as plain text.
 type textPrinter struct {
        *Printer
-       long   bytes.Buffer
+       long   strings.Builder
        prefix string
        width  int
 }
@@ -81,6 +81,11 @@ func (p *textPrinter) block(out *bytes.Buffer, x Block) {
        case *Paragraph:
                out.WriteString(p.prefix)
                p.text(out, x.Text)
+
+       case *Heading:
+               out.WriteString(p.prefix)
+               out.WriteString("# ")
+               p.text(out, x.Text)
        }
 }
 
@@ -114,7 +119,7 @@ func (p *textPrinter) text(out *bytes.Buffer, x []Text) {
 // oneLongLine prints the text sequence x to out as one long line,
 // without worrying about line wrapping.
 // Explicit links have the [ ] dropped to improve readability.
-func (p *textPrinter) oneLongLine(out *bytes.Buffer, x []Text) {
+func (p *textPrinter) oneLongLine(out *strings.Builder, x []Text) {
        for _, t := range x {
                switch t := t.(type) {
                case Plain: