зеркало из https://github.com/golang/pkgsite.git
internal/godoc/dochtml/internal/render: unique headings redux
This CL takes an alternative approach to generate unique headings. It builds on the work of https://go.dev/cl/573595, which was a fix for https://go.dev/issue/64582. - It takes a more direct approach to constructing a unique string from an ast.Decl. - The function that does that is tested separately, reducing the test cases needed for formatDocHTML. - It saves the generated headings, so the HTML doesn't have to be reparsed. NOTE: This will break all links to headings that are not in the package comment. Happily, such headings are rare: only 40 of the top thousand packages have one. It might seem we could avoid any breakage by only applying a suffix to duplicate headings. But then at any time in the future, a unique heading could become a duplicate, causing a break. Better one break now than an unending stream of them. Change-Id: I379712b54c6bc9c6a9343d0006639085a40e23d9 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/608035 kokoro-CI: kokoro <noreply+kokoro@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
Родитель
46f6a4c98c
Коммит
47024e5792
|
@ -23,7 +23,6 @@ import (
|
|||
safe "github.com/google/safehtml"
|
||||
"github.com/google/safehtml/legacyconversions"
|
||||
"github.com/google/safehtml/template"
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/pkgsite/internal/log"
|
||||
)
|
||||
|
||||
|
@ -53,10 +52,7 @@ const (
|
|||
rfcRx = `RFC\s+(\d{3,5})(,?\s+[Ss]ection\s+(\d+(\.\d+)*))?`
|
||||
)
|
||||
|
||||
var (
|
||||
matchRx = regexp.MustCompile(urlRx + `|` + rfcRx)
|
||||
badAnchorRx = regexp.MustCompile(`[^a-zA-Z0-9]`)
|
||||
)
|
||||
var matchRx = regexp.MustCompile(urlRx + `|` + rfcRx)
|
||||
|
||||
type link struct {
|
||||
Class string
|
||||
|
@ -65,7 +61,7 @@ type link struct {
|
|||
}
|
||||
|
||||
type heading struct {
|
||||
ID safe.Identifier
|
||||
ID safe.Identifier // if empty, the title is not linked
|
||||
Title safe.HTML
|
||||
}
|
||||
|
||||
|
@ -73,11 +69,15 @@ var (
|
|||
// tocTemplate expects a []heading.
|
||||
tocTemplate = template.Must(template.New("toc").Parse(`<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc{{if gt (len .) 5}} Documentation-toc-columns{{end}}">
|
||||
{{range . -}}
|
||||
{{- range .}}
|
||||
<li class="Documentation-tocItem">
|
||||
{{- if .ID.String -}}
|
||||
<a href="#{{.ID}}">{{.Title}}</a>
|
||||
{{- else -}}
|
||||
{{.Title}}
|
||||
{{- end -}}
|
||||
</li>
|
||||
{{end -}}
|
||||
{{- end}}
|
||||
</ul>
|
||||
</div>
|
||||
`))
|
||||
|
@ -88,8 +88,13 @@ var (
|
|||
|
||||
paraTemplate = template.Must(template.New("para").Parse("<p>{{.}}\n</p>"))
|
||||
|
||||
headingTemplate = template.Must(template.New("heading").Parse(
|
||||
`<h4 id="{{.ID}}">{{.Title}} <a class="Documentation-idLink" href="#{{.ID}}" aria-label="Go to {{.Title}}">¶</a></h4>`))
|
||||
// expects a heading
|
||||
headingTemplate = template.Must(template.New("heading").Parse(`
|
||||
{{- if .ID.String -}}
|
||||
<h4 id="{{.ID}}">{{.Title}} <a class="Documentation-idLink" href="#{{.ID}}" aria-label="Go to {{.Title}}">¶</a></h4>
|
||||
{{- else -}}
|
||||
<h4>{{.Title}}</h4>
|
||||
{{- end}}`))
|
||||
|
||||
linkTemplate = template.Must(template.New("link").Parse(
|
||||
`<a{{with .Class}}class="{{.}}" {{end}} href="{{.Href}}">{{.Text}}</a>`))
|
||||
|
@ -119,57 +124,14 @@ func (r *Renderer) formatDocHTML(text string, decl ast.Decl, extractLinks bool)
|
|||
if extractLinks {
|
||||
r.removeLinks(doc)
|
||||
}
|
||||
|
||||
h := r.blocksToHTML(doc.Content, decl, true, extractLinks)
|
||||
if headings := extractHeadings(h); len(headings) > 0 {
|
||||
h = safe.HTMLConcat(ExecuteToHTML(tocTemplate, headings), h)
|
||||
hscope := newHeadingScope(headingIDSuffix(decl))
|
||||
h := r.blocksToHTML(doc.Content, true, hscope)
|
||||
if len(hscope.headings) > 0 {
|
||||
h = safe.HTMLConcat(ExecuteToHTML(tocTemplate, hscope.headings), h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func extractHeadings(h safe.HTML) []heading {
|
||||
var headings []heading
|
||||
|
||||
doc, err := html.Parse(strings.NewReader(h.String()))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var f func(*html.Node)
|
||||
f = func(n *html.Node) {
|
||||
for _, a := range n.Attr {
|
||||
if a.Key == "id" && strings.HasPrefix(a.Val, "hdr-") {
|
||||
if tn := firstTextNode(n); tn != nil {
|
||||
title := strings.TrimSpace(tn.Data)
|
||||
hdrName := strings.TrimPrefix(a.Val, "hdr-")
|
||||
id := safe.IdentifierFromConstantPrefix("hdr", hdrName)
|
||||
headings = append(headings, heading{id, safe.HTMLEscaped(title)})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
f(doc)
|
||||
|
||||
return headings
|
||||
}
|
||||
|
||||
func firstTextNode(n *html.Node) *html.Node {
|
||||
if n.Type == html.TextNode {
|
||||
return n
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if r := firstTextNode(c); r != nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeLinks removes the "Links" section from doc.
|
||||
// Pkgsite has a convention where a "Links" heading in a doc comment provides links
|
||||
// that are rendered in a separate place in the UI.
|
||||
|
@ -263,13 +225,13 @@ func parseLink(line string) *Link {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) blocksToHTML(bs []comment.Block, decl ast.Decl, useParagraph, extractLinks bool) safe.HTML {
|
||||
func (r *Renderer) blocksToHTML(bs []comment.Block, useParagraph bool, hscope *headingScope) safe.HTML {
|
||||
return concatHTML(bs, func(b comment.Block) safe.HTML {
|
||||
return r.blockToHTML(b, decl, useParagraph, extractLinks)
|
||||
return r.blockToHTML(b, useParagraph, hscope)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Renderer) blockToHTML(b comment.Block, decl ast.Decl, useParagraph, extractLinks bool) safe.HTML {
|
||||
func (r *Renderer) blockToHTML(b comment.Block, useParagraph bool, hscope *headingScope) safe.HTML {
|
||||
switch b := b.(type) {
|
||||
case *comment.Paragraph:
|
||||
th := r.textsToHTML(b.Text)
|
||||
|
@ -282,7 +244,7 @@ func (r *Renderer) blockToHTML(b comment.Block, decl ast.Decl, useParagraph, ext
|
|||
return ExecuteToHTML(codeTemplate, b.Text)
|
||||
|
||||
case *comment.Heading:
|
||||
return ExecuteToHTML(headingTemplate, r.newHeading(b, decl))
|
||||
return ExecuteToHTML(headingTemplate, r.newHeading(b, hscope))
|
||||
|
||||
case *comment.List:
|
||||
var items []safe.HTML
|
||||
|
@ -291,7 +253,7 @@ func (r *Renderer) blockToHTML(b comment.Block, decl ast.Decl, useParagraph, ext
|
|||
items = append(items, ExecuteToHTML(listItemTemplate, struct {
|
||||
Number string
|
||||
Content safe.HTML
|
||||
}{item.Number, r.blocksToHTML(item.Content, decl, useParagraph, false)}))
|
||||
}{item.Number, r.blocksToHTML(item.Content, useParagraph, hscope)}))
|
||||
}
|
||||
t := oListTemplate
|
||||
if b.Items[0].Number == "" {
|
||||
|
@ -303,53 +265,143 @@ func (r *Renderer) blockToHTML(b comment.Block, decl ast.Decl, useParagraph, ext
|
|||
}
|
||||
}
|
||||
|
||||
// headingIDs keeps track of used heading ids to prevent duplicates.
|
||||
type headingIDs map[safe.Identifier]bool
|
||||
/*
|
||||
We want to convert headings in doc comments to unique and stable HTML IDs.
|
||||
|
||||
// headingID returns a unique identifier for a *comment.Heading & ast.Decl pair.
|
||||
func (r *Renderer) headingID(h *comment.Heading, decl ast.Decl) safe.Identifier {
|
||||
s := textsToString(h.Text)
|
||||
hdrTitle := badAnchorRx.ReplaceAllString(s, "_")
|
||||
id := safe.IdentifierFromConstantPrefix("hdr", hdrTitle)
|
||||
if !r.headingIDs[id] {
|
||||
r.headingIDs[id] = true
|
||||
return id
|
||||
}
|
||||
The IDs in an HTML page should be unique, so URLs with fragments can link to
|
||||
them. The headings in the doc comments of a Go package (which corresponds to an
|
||||
HTML page for us) need not be unique. The same heading can be used in the doc
|
||||
comment of several symbols, and in the package comment as well.
|
||||
|
||||
// The id is not unique. Attempt to generate an identifier using the decl.
|
||||
for _, v := range generateAnchorPoints(decl) {
|
||||
if v.Kind == "field" {
|
||||
continue
|
||||
}
|
||||
hdrTitle = badAnchorRx.ReplaceAllString(v.ID.String()+"_"+s, "_")
|
||||
if v.Kind == "constant" || v.Kind == "variable" {
|
||||
if specs := decl.(*ast.GenDecl).Specs; len(specs) > 1 {
|
||||
// Grouped consts and vars cannot be deterministically identified,
|
||||
// so treat them as a single section. e.g. "hdr-constant-Title"
|
||||
hdrTitle = badAnchorRx.ReplaceAllString(v.Kind+"_"+s, "_")
|
||||
}
|
||||
}
|
||||
if v.Kind != "method" {
|
||||
// Continue iterating until we find a higher precedence kind.
|
||||
break
|
||||
}
|
||||
}
|
||||
If uniqueness were the only criterion, we could just append a number. But we
|
||||
also want the IDs to be stable. The page for the latest version of a package
|
||||
has no version in the URL, so that a saved link will always reference the latest
|
||||
version. We do the same for other links on the page, like links to symbols. We
|
||||
want to do so for heading links too. That means that a heading link, which uses
|
||||
the ID as a fragment, may visit pages with different content at different times.
|
||||
We want that saved link to return to the same heading every time, if the heading
|
||||
still exists.
|
||||
|
||||
for {
|
||||
id = safe.IdentifierFromConstantPrefix("hdr", hdrTitle)
|
||||
if !r.headingIDs[id] {
|
||||
r.headingIDs[id] = true
|
||||
break
|
||||
}
|
||||
// The id is still not unique. Append _ until unique.
|
||||
hdrTitle = hdrTitle + "_"
|
||||
}
|
||||
If we numbered the IDs from top to bottom, a heading ID would change whenever headings
|
||||
were added above it. Instead, we try to construct a unique suffix from the declaration that the
|
||||
doc comment refers to. (The suffix is empty for the package comment.) For ungrouped declarations,
|
||||
we use the symbol name, which must be unique in the package. For grouped declarations, like
|
||||
|
||||
const (
|
||||
A = 1
|
||||
B = 2
|
||||
)
|
||||
|
||||
or
|
||||
|
||||
var x, Y int
|
||||
|
||||
there is no good way to pick a suffix, so we don't link headings at all. Evidence suggests
|
||||
that such headings are very rare.
|
||||
|
||||
After constructing the unique suffix, we may still have duplicate IDs because the same doc comment
|
||||
can repeat headings. So we append a number that is unique for the given suffix.
|
||||
|
||||
This approach will always produce unique, stable IDs when it produces IDs at all.
|
||||
*/
|
||||
|
||||
// A headingScope is a collection of headings that arise from the documentation of
|
||||
// a single element, either the package itself or a symbol within it.
|
||||
// A headingScope ensures that heading IDs are unique, and keeps a list of headings as they appear in the scope.
|
||||
type headingScope struct {
|
||||
suffix string // ID suffix; unique for the package; empty for package doc
|
||||
createIDs bool // create IDs for the headings
|
||||
headings []heading // headings for this scope, in order
|
||||
ids map[string]int // count of seen IDs
|
||||
}
|
||||
|
||||
func newHeadingScope(suffix string, createIDs bool) *headingScope {
|
||||
return &headingScope{
|
||||
suffix: badAnchorRx.ReplaceAllString(suffix, "_"),
|
||||
createIDs: createIDs,
|
||||
ids: map[string]int{},
|
||||
}
|
||||
}
|
||||
|
||||
// newHeading constructs a heading from the comment.Heading and remembers it.
|
||||
func (r *Renderer) newHeading(h *comment.Heading, hs *headingScope) heading {
|
||||
return hs.addHeading(h, r.textsToHTML(h.Text))
|
||||
}
|
||||
|
||||
// addHeading constructs a heading and adds it to hs.
|
||||
func (hs *headingScope) addHeading(ch *comment.Heading, html safe.HTML) heading {
|
||||
h := heading{Title: html}
|
||||
if hs.createIDs {
|
||||
h.ID = safe.IdentifierFromConstantPrefix("hdr", hs.newHeadingID(ch))
|
||||
}
|
||||
hs.headings = append(hs.headings, h)
|
||||
return h
|
||||
}
|
||||
|
||||
var badAnchorRx = regexp.MustCompile(`[^a-zA-Z0-9-]`)
|
||||
|
||||
// newHeadingID constructs a unique heading ID from the argument, which is the text
|
||||
// of a heading.
|
||||
func (hs *headingScope) newHeadingID(ch *comment.Heading) string {
|
||||
// Start with the heading's default ID.
|
||||
// We can only construct a [safe.Identifier] by adding a prefix, so remove the
|
||||
// default's prefix.
|
||||
id := strings.TrimPrefix(ch.DefaultID(), "hdr-")
|
||||
if hs.suffix != "" {
|
||||
id += "-" + hs.suffix
|
||||
}
|
||||
n := hs.ids[id]
|
||||
hs.ids[id]++
|
||||
if n > 0 {
|
||||
id += "-" + strconv.Itoa(n)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (r *Renderer) newHeading(h *comment.Heading, decl ast.Decl) heading {
|
||||
return heading{r.headingID(h, decl), r.textsToHTML(h.Text)}
|
||||
// headingIDSuffix returns a unique string from the decl, to be
|
||||
// used as the suffix in heading IDs.
|
||||
// If a unique string can be created, the second return value is true.
|
||||
// If not (the declaration is a group), headingIDSuffix returns "", false.
|
||||
func headingIDSuffix(decl ast.Decl) (string, bool) {
|
||||
if decl == nil {
|
||||
return "", true
|
||||
}
|
||||
switch decl := decl.(type) {
|
||||
case *ast.FuncDecl:
|
||||
// functionName, or recvTypeName_methodName
|
||||
if decl.Recv == nil || len(decl.Recv.List) == 0 {
|
||||
return decl.Name.Name, true
|
||||
}
|
||||
return recvTypeString(decl.Recv.List[0].Type) + "_" + decl.Name.Name, true
|
||||
case *ast.GenDecl:
|
||||
// We can only pick a stable name when there is a single decl.
|
||||
if len(decl.Specs) == 1 {
|
||||
switch spec := decl.Specs[0].(type) {
|
||||
case *ast.TypeSpec:
|
||||
return spec.Name.Name, true
|
||||
case *ast.ValueSpec:
|
||||
if len(spec.Names) == 1 {
|
||||
return spec.Names[0].Name, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// recvTypeString returns a string for the symbol in a type expression
|
||||
// for a method receiver.
|
||||
func recvTypeString(t ast.Expr) string {
|
||||
switch t := t.(type) {
|
||||
case *ast.Ident:
|
||||
return t.Name
|
||||
case *ast.IndexExpr:
|
||||
return t.X.(*ast.Ident).Name
|
||||
case *ast.StarExpr:
|
||||
return recvTypeString(t.X)
|
||||
default: // should never happen
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) textsToHTML(ts []comment.Text) safe.HTML {
|
||||
|
|
|
@ -9,14 +9,17 @@ import (
|
|||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/doc/comment"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/safehtml"
|
||||
safe "github.com/google/safehtml"
|
||||
"github.com/google/safehtml/testconversions"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
@ -56,10 +59,14 @@ func TestFormatDocHTML(t *testing.T) {
|
|||
|
||||
doc := string(mustContent(t, "doc"))
|
||||
wantNoExtract := mustContent(t, "want")
|
||||
var decl ast.Decl
|
||||
if d := getContent("decl"); d != "" {
|
||||
decl = parseDecl(t, d)
|
||||
}
|
||||
for _, extractLinks := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("extractLinks=%t", extractLinks), func(t *testing.T) {
|
||||
r := New(context.Background(), nil, pkgTime, nil)
|
||||
got := r.formatDocHTML(doc, nil, extractLinks).String()
|
||||
got := r.formatDocHTML(doc, decl, extractLinks).String()
|
||||
want := wantNoExtract
|
||||
wantLinks := ""
|
||||
if extractLinks {
|
||||
|
@ -71,6 +78,8 @@ func TestFormatDocHTML(t *testing.T) {
|
|||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("doc mismatch (-want +got)\n%s", diff)
|
||||
t.Logf("want: %s", want)
|
||||
t.Logf("got: %s", got)
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, l := range r.Links() {
|
||||
|
@ -85,240 +94,6 @@ func TestFormatDocHTML(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFormatDocHTMLDecl(t *testing.T) {
|
||||
duplicateHeadersDoc := `Documentation.
|
||||
|
||||
Information
|
||||
|
||||
This is some information.
|
||||
|
||||
Information
|
||||
|
||||
This is some other information.
|
||||
`
|
||||
// typeWithFieldsDecl is declared as:
|
||||
// type I2 interface {
|
||||
// I1
|
||||
// M2()
|
||||
// }
|
||||
typeWithFieldsDecl := &ast.GenDecl{
|
||||
Tok: token.TYPE,
|
||||
Specs: []ast.Spec{
|
||||
&ast.TypeSpec{
|
||||
Name: ast.NewIdent("I2"),
|
||||
Type: &ast.InterfaceType{
|
||||
Methods: &ast.FieldList{
|
||||
List: []*ast.Field{
|
||||
{Type: ast.NewIdent("I1")},
|
||||
{Type: &ast.FuncType{}, Names: []*ast.Ident{ast.NewIdent("M2")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
doc string
|
||||
decl ast.Decl
|
||||
extractLinks []bool // nil means both
|
||||
want string
|
||||
wantLinks []Link
|
||||
}{
|
||||
{
|
||||
name: "unique header ids in constants section for grouped constants",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.GenDecl{
|
||||
Tok: token.CONST,
|
||||
Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{{}}}, &ast.ValueSpec{Names: []*ast.Ident{{}}}},
|
||||
},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-constant_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-constant_Information">Information <a class="Documentation-idLink" href="#hdr-constant_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in variables section for grouped variables",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.GenDecl{
|
||||
Tok: token.VAR,
|
||||
Specs: []ast.Spec{&ast.ValueSpec{Names: []*ast.Ident{{}}}, &ast.ValueSpec{Names: []*ast.Ident{{}}}},
|
||||
},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-variable_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-variable_Information">Information <a class="Documentation-idLink" href="#hdr-variable_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in functions section",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.FuncDecl{Name: ast.NewIdent("FooFunc")},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-FooFunc_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-FooFunc_Information">Information <a class="Documentation-idLink" href="#hdr-FooFunc_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in functions section for method",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.FuncDecl{
|
||||
Recv: &ast.FieldList{List: []*ast.Field{{Type: ast.NewIdent("Bar")}}},
|
||||
Name: ast.NewIdent("Func"),
|
||||
},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Bar_Func_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Bar_Func_Information">Information <a class="Documentation-idLink" href="#hdr-Bar_Func_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in types section",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.GenDecl{
|
||||
Tok: token.TYPE,
|
||||
Specs: []ast.Spec{&ast.TypeSpec{Name: ast.NewIdent("Duration")}},
|
||||
},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Duration_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Duration_Information">Information <a class="Documentation-idLink" href="#hdr-Duration_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in types section for types with fields",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: typeWithFieldsDecl,
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-I2_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-I2_Information">Information <a class="Documentation-idLink" href="#hdr-I2_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in types section for typed variable",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.GenDecl{
|
||||
Tok: token.VAR,
|
||||
Specs: []ast.Spec{&ast.ValueSpec{Type: &ast.StarExpr{X: ast.NewIdent("Location")}, Names: []*ast.Ident{ast.NewIdent("UTC")}}},
|
||||
},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-UTC_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-UTC_Information">Information <a class="Documentation-idLink" href="#hdr-UTC_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
{
|
||||
name: "unique header ids in types section for typed constant",
|
||||
doc: duplicateHeadersDoc,
|
||||
decl: &ast.GenDecl{
|
||||
Tok: token.CONST,
|
||||
Specs: []ast.Spec{&ast.ValueSpec{Type: ast.NewIdent("T"), Names: []*ast.Ident{ast.NewIdent("C")}}},
|
||||
},
|
||||
want: `<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-C_Information">Information</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-C_Information">Information <a class="Documentation-idLink" href="#hdr-C_Information" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p>`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
extractLinks := test.extractLinks
|
||||
if extractLinks == nil {
|
||||
extractLinks = []bool{false, true}
|
||||
}
|
||||
for _, el := range extractLinks {
|
||||
t.Run(fmt.Sprintf("extractLinks=%t", el), func(t *testing.T) {
|
||||
r := New(context.Background(), nil, pkgTime, nil)
|
||||
got := r.formatDocHTML(test.doc, test.decl, el)
|
||||
want := testconversions.MakeHTMLForTest(test.want)
|
||||
if diff := cmp.Diff(want, got, cmp.AllowUnexported(safehtml.HTML{})); diff != "" {
|
||||
t.Errorf("doc mismatch (-want +got)\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(test.wantLinks, r.Links()); diff != "" {
|
||||
t.Errorf("r.Links() mismatch (-want +got)\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclHTML(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
@ -711,12 +486,8 @@ More text.`
|
|||
|
||||
want := testconversions.MakeHTMLForTest(`<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-The_Go_Project">The Go Project</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Heading_2">Heading 2</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-The_Go_Project">The Go Project</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Heading_2">Heading 2</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
|
@ -730,3 +501,98 @@ More text.`
|
|||
t.Errorf("r.declHTML() mismatch (-want +got)\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadingIDSuffix(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
decl string
|
||||
want string
|
||||
wantIDs bool
|
||||
}{
|
||||
{"", "", true},
|
||||
{"func Foo(){}", "Foo", true},
|
||||
{"func (x T) Run(){}", "T_Run", true},
|
||||
{"func (x *T) Run(){}", "T_Run", true},
|
||||
{"func (x T[A]) Run(){}", "T_Run", true},
|
||||
{"func (x *T[A]) Run(){}", "T_Run", true},
|
||||
{"const C = 1", "C", true},
|
||||
{"var V int", "V", true},
|
||||
{"var V, W int", "", false},
|
||||
{"var x, y, V int", "", false},
|
||||
{"type T int", "T", true},
|
||||
{"type T_Run[X any] int", "T_Run", true},
|
||||
{"const (a = 1; b = 2; C = 3)", "", false},
|
||||
{"var (a = 1; b = 2; C = 3)", "", false},
|
||||
{"type (a int; b int; C int)", "", false},
|
||||
} {
|
||||
var decl ast.Decl
|
||||
if test.decl != "" {
|
||||
decl = parseDecl(t, test.decl)
|
||||
}
|
||||
got, gotIDs := headingIDSuffix(decl)
|
||||
if got != test.want {
|
||||
t.Errorf("%q: got %q, want %q", test.decl, got, test.want)
|
||||
}
|
||||
if gotIDs != test.wantIDs {
|
||||
t.Errorf("%q createIDs: got %t, want %t", test.decl, gotIDs, test.wantIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseDecl(t *testing.T, decl string) ast.Decl {
|
||||
prog := "package p\n" + decl
|
||||
f, err := parser.ParseFile(token.NewFileSet(), "", prog, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f.Decls[0]
|
||||
}
|
||||
|
||||
func TestAddHeading(t *testing.T) {
|
||||
// This test checks that the generated IDs are unique and the headings are saved.
|
||||
// It doesn't care about the HTML.
|
||||
var html safe.HTML
|
||||
|
||||
check := func(hs *headingScope, ids ...string) {
|
||||
t.Helper()
|
||||
var want []heading
|
||||
for _, id := range ids {
|
||||
want = append(want, heading{safe.IdentifierFromConstantPrefix("hdr", id), html})
|
||||
}
|
||||
if !slices.Equal(hs.headings, want) {
|
||||
t.Errorf("\ngot %v\nwant %v", hs.headings, want)
|
||||
}
|
||||
}
|
||||
|
||||
addHeading := func(hs *headingScope, heading string) {
|
||||
hs.addHeading(&comment.Heading{
|
||||
Text: []comment.Text{comment.Plain(heading)},
|
||||
}, html)
|
||||
}
|
||||
|
||||
hs := newHeadingScope("T", true)
|
||||
addHeading(hs, "heading")
|
||||
addHeading(hs, "heading 2")
|
||||
addHeading(hs, "heading")
|
||||
addHeading(hs, "heading")
|
||||
addHeading(hs, "heading.2")
|
||||
check(hs, "heading-T", "heading_2-T", "heading-T-1", "heading-T-2", "heading_2-T-1")
|
||||
|
||||
// Check empty suffix.
|
||||
hs = newHeadingScope("", true)
|
||||
addHeading(hs, "h")
|
||||
addHeading(hs, "h")
|
||||
check(hs, "h", "h-1")
|
||||
|
||||
// Check that invalid ID characters are removed from both suffix and input.
|
||||
hs = newHeadingScope("a.b𝜽", true)
|
||||
addHeading(hs, "h.i𝜽")
|
||||
check(hs, "h_i_-a_b_")
|
||||
|
||||
// Check no link (empty ID).
|
||||
hs = newHeadingScope("", false)
|
||||
addHeading(hs, "h")
|
||||
want := []heading{{Title: html}}
|
||||
if !slices.Equal(hs.headings, want) {
|
||||
t.Errorf("got %v, want %v", hs.headings, want)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ var (
|
|||
|
||||
type Renderer struct {
|
||||
fset *token.FileSet
|
||||
headingIDs headingIDs
|
||||
pids *packageIDs
|
||||
packageURL func(string) string
|
||||
ctx context.Context
|
||||
|
@ -98,7 +97,6 @@ func New(ctx context.Context, fset *token.FileSet, pkg *doc.Package, opts *Optio
|
|||
|
||||
return &Renderer{
|
||||
fset: fset,
|
||||
headingIDs: headingIDs{},
|
||||
pids: pids,
|
||||
packageURL: packageURL,
|
||||
docTmpl: docDataTmpl,
|
||||
|
|
|
@ -12,5 +12,6 @@ By default, "want" is tested with extractLinks set to both true and false.
|
|||
The following sections are optional:
|
||||
|
||||
- want:links: the output when extractLinks = true
|
||||
- links: must be present if want:links is present; the extracted
|
||||
links, one per line, each line has text and href separated by a single space.
|
||||
- links: the extracted links, one per line
|
||||
Each line has text and href separated by a single space.
|
||||
- decl: A Go declaration to be passed to formatDocHTML. Default is nil.
|
||||
|
|
|
@ -12,9 +12,7 @@ Go is an open source project.
|
|||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-The_Go_Project2">The Go Project2</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-The_Go_Project2">The Go Project2</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>The Go Project1
|
||||
|
|
|
@ -20,15 +20,9 @@ More doc.
|
|||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-The_Go_Project">The Go Project</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Links">Links</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Header">Header</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-The_Go_Project">The Go Project</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Links">Links</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Header">Header</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
|
@ -41,12 +35,8 @@ More doc.
|
|||
-- want:links --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-The_Go_Project">The Go Project</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Header">Header</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-The_Go_Project">The Go Project</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Header">Header</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
|
|
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unique_func.txt
поставляемый
Normal file
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unique_func.txt
поставляемый
Normal file
|
@ -0,0 +1,25 @@
|
|||
Unique heading IDs in function declaration.
|
||||
-- doc --
|
||||
Documentation.
|
||||
|
||||
Info
|
||||
|
||||
This is some information.
|
||||
|
||||
Info
|
||||
|
||||
This is some other information.
|
||||
-- decl --
|
||||
func Run() {}
|
||||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-Run">Info</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-Run-1">Info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Info-Run">Info <a class="Documentation-idLink" href="#hdr-Info-Run" aria-label="Go to Info">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Info-Run-1">Info <a class="Documentation-idLink" href="#hdr-Info-Run-1" aria-label="Go to Info">¶</a></h4><p>This is some other information.
|
||||
</p>
|
||||
|
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unique_method.txt
поставляемый
Normal file
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unique_method.txt
поставляемый
Normal file
|
@ -0,0 +1,25 @@
|
|||
Unique heading IDs in method declaration.
|
||||
-- doc --
|
||||
Documentation.
|
||||
|
||||
Info
|
||||
|
||||
This is some information.
|
||||
|
||||
Info
|
||||
|
||||
This is some other information.
|
||||
-- decl --
|
||||
func (T) M() {}
|
||||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-T_M">Info</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-T_M-1">Info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Info-T_M">Info <a class="Documentation-idLink" href="#hdr-Info-T_M" aria-label="Go to Info">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Info-T_M-1">Info <a class="Documentation-idLink" href="#hdr-Info-T_M-1" aria-label="Go to Info">¶</a></h4><p>This is some other information.
|
||||
</p>
|
||||
|
|
@ -1,27 +1,23 @@
|
|||
Unique header IDs in overview.
|
||||
Unique heading IDs in overview.
|
||||
-- doc --
|
||||
Documentation.
|
||||
|
||||
Information
|
||||
Info
|
||||
|
||||
This is some information.
|
||||
|
||||
Information
|
||||
Info
|
||||
|
||||
This is some other information.
|
||||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Information_">Information</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info">Info</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-1">Info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Information">Information <a class="Documentation-idLink" href="#hdr-Information" aria-label="Go to Information">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Information_">Information <a class="Documentation-idLink" href="#hdr-Information_" aria-label="Go to Information">¶</a></h4><p>This is some other information.
|
||||
</p><h4 id="hdr-Info">Info <a class="Documentation-idLink" href="#hdr-Info" aria-label="Go to Info">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Info-1">Info <a class="Documentation-idLink" href="#hdr-Info-1" aria-label="Go to Info">¶</a></h4><p>This is some other information.
|
||||
</p>
|
||||
|
||||
|
|
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unique_var.txt
поставляемый
Normal file
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unique_var.txt
поставляемый
Normal file
|
@ -0,0 +1,25 @@
|
|||
Unique heading IDs in variable declaration.
|
||||
-- doc --
|
||||
Documentation.
|
||||
|
||||
Info
|
||||
|
||||
This is some information.
|
||||
|
||||
Info
|
||||
|
||||
This is some other information.
|
||||
-- decl --
|
||||
var Global int
|
||||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-Global">Info</a></li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Info-Global-1">Info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4 id="hdr-Info-Global">Info <a class="Documentation-idLink" href="#hdr-Info-Global" aria-label="Go to Info">¶</a></h4><p>This is some information.
|
||||
</p><h4 id="hdr-Info-Global-1">Info <a class="Documentation-idLink" href="#hdr-Info-Global-1" aria-label="Go to Info">¶</a></h4><p>This is some other information.
|
||||
</p>
|
||||
|
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unlinked_group.txt
поставляемый
Normal file
25
internal/godoc/dochtml/internal/render/testdata/formatDocHTML/unlinked_group.txt
поставляемый
Normal file
|
@ -0,0 +1,25 @@
|
|||
Unlinked heading IDs in grouped declaration.
|
||||
-- doc --
|
||||
Documentation.
|
||||
|
||||
Info
|
||||
|
||||
This is some information.
|
||||
|
||||
Info
|
||||
|
||||
This is some other information.
|
||||
-- decl --
|
||||
var A, B int
|
||||
-- want --
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">Info</li>
|
||||
<li class="Documentation-tocItem">Info</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>Documentation.
|
||||
</p><h4>Info</h4><p>This is some information.
|
||||
</p><h4>Info</h4><p>This is some other information.
|
||||
</p>
|
||||
|
|
@ -33,9 +33,7 @@ This refers to the standard library <a href="/encoding/json">encoding/json</a> p
|
|||
</div>
|
||||
<div role="navigation" aria-label="Table of Contents">
|
||||
<ul class="Documentation-toc">
|
||||
<li class="Documentation-tocItem">
|
||||
<a href="#hdr-Example">Example</a>
|
||||
</li>
|
||||
<li class="Documentation-tocItem"><a href="#hdr-Example-F">Example</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>F refers to function <a href="#G">G</a> and method <a href="#T.M">T.M</a>.
|
||||
|
@ -44,7 +42,7 @@ It also has three bullet points:
|
|||
<li>one</li>
|
||||
<li>two</li>
|
||||
<li>three</li>
|
||||
</ul><h4 id="hdr-Example">Example <a class="Documentation-idLink" href="#hdr-Example" aria-label="Go to Example">¶</a></h4><p>Here is an example:
|
||||
</ul><h4 id="hdr-Example-F">Example <a class="Documentation-idLink" href="#hdr-Example-F" aria-label="Go to Example">¶</a></h4><p>Here is an example:
|
||||
</p><pre>F()
|
||||
</pre>
|
||||
</div><div class="Documentation-function">
|
||||
|
|
Загрузка…
Ссылка в новой задаче