Skip to content

Commit 919039a

Browse files
authored
fix(internal/godocfx): use correct anchor links (#3738)
This works by pre-processing the package and generating all of the correct anchor links. Then, when converting an ID to a link, we check if the ID is listed in the map of anchors. It's possible there will be some duplication between IDs. But, that is already an issue because there is no way to know which one is correct to link to. An alternative would be to set the anchor for any duplicated links to `""` so they don't link at all. In practice, I don't think this is an issue. So, I left it as-is and we can revisit later if needed. Fixes #3737.
1 parent 8cc52d1 commit 919039a

File tree

2 files changed

+360
-239
lines changed

2 files changed

+360
-239
lines changed

internal/godocfx/parse.go

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"log"
3535
"os"
3636
"path/filepath"
37+
"regexp"
3738
"sort"
3839
"strconv"
3940
"strings"
@@ -155,7 +156,7 @@ func parse(glob string, workingDir string, optionalExtraFiles []string) (*result
155156
// Once the files are grouped by package, process each package
156157
// independently.
157158
for _, pi := range pkgInfos {
158-
link := newLinker(pi.pkg.Imports, pi.importRenames)
159+
link := newLinker(pi)
159160
topLevelDecls := pkgsite.TopLevelDecls(pi.doc)
160161
pkgItem := &item{
161162
UID: pi.doc.ImportPath,
@@ -183,7 +184,7 @@ func parse(glob string, workingDir string, optionalExtraFiles []string) (*result
183184
Type: "const",
184185
Summary: c.Doc,
185186
Langs: onlyGo,
186-
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, c.Decl, toURL, topLevelDecls)},
187+
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, c.Decl, link.toURL, topLevelDecls)},
187188
})
188189
}
189190
for _, v := range pi.doc.Vars {
@@ -199,7 +200,7 @@ func parse(glob string, workingDir string, optionalExtraFiles []string) (*result
199200
Type: "variable",
200201
Summary: v.Doc,
201202
Langs: onlyGo,
202-
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, v.Decl, toURL, topLevelDecls)},
203+
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, v.Decl, link.toURL, topLevelDecls)},
203204
})
204205
}
205206
for _, t := range pi.doc.Types {
@@ -213,7 +214,7 @@ func parse(glob string, workingDir string, optionalExtraFiles []string) (*result
213214
Type: "type",
214215
Summary: t.Doc,
215216
Langs: onlyGo,
216-
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, t.Decl, toURL, topLevelDecls)},
217+
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, t.Decl, link.toURL, topLevelDecls)},
217218
Examples: processExamples(t.Examples, pi.fset),
218219
}
219220
// Note: items are added as page.Children, rather than
@@ -232,7 +233,7 @@ func parse(glob string, workingDir string, optionalExtraFiles []string) (*result
232233
Type: "const",
233234
Summary: c.Doc,
234235
Langs: onlyGo,
235-
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, c.Decl, toURL, topLevelDecls)},
236+
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, c.Decl, link.toURL, topLevelDecls)},
236237
})
237238
}
238239
for _, v := range t.Vars {
@@ -248,7 +249,7 @@ func parse(glob string, workingDir string, optionalExtraFiles []string) (*result
248249
Type: "variable",
249250
Summary: v.Doc,
250251
Langs: onlyGo,
251-
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, v.Decl, toURL, topLevelDecls)},
252+
Syntax: syntax{Content: pkgsite.PrintType(pi.fset, v.Decl, link.toURL, topLevelDecls)},
252253
})
253254
}
254255

@@ -313,18 +314,84 @@ type linker struct {
313314
// Behavior is undefined when a single import has different names in
314315
// different files.
315316
imports map[string]string
317+
318+
// idToAnchor is a map from an ID to the anchor URL for that ID.
319+
idToAnchor map[string]string
316320
}
317321

318-
func newLinker(rawImports map[string]*packages.Package, importSyntax map[string]string) *linker {
322+
func newLinker(pi pkgInfo) *linker {
319323
imports := map[string]string{}
320-
for path, pkg := range rawImports {
324+
for path, pkg := range pi.pkg.Imports {
321325
name := pkg.Name
322-
if rename := importSyntax[path]; rename != "" {
326+
if rename := pi.importRenames[path]; rename != "" {
323327
name = rename
324328
}
325329
imports[name] = path
326330
}
327-
return &linker{imports: imports}
331+
332+
idToAnchor := buildIDToAnchor(pi)
333+
334+
return &linker{imports: imports, idToAnchor: idToAnchor}
335+
}
336+
337+
// nonWordRegex is based on
338+
// https://github.com/googleapis/doc-templates/blob/70eba5908e7b9aef5525d0f1f24194ae750f267e/third_party/docfx/templates/devsite/common.js#L27-L30.
339+
var nonWordRegex = regexp.MustCompile("\\W")
340+
341+
func buildIDToAnchor(pi pkgInfo) map[string]string {
342+
idToAnchor := map[string]string{}
343+
idToAnchor[pi.doc.ImportPath] = pi.doc.ImportPath
344+
345+
for _, c := range pi.doc.Consts {
346+
commaID := strings.Join(c.Names, ",")
347+
uid := pi.doc.ImportPath + "." + commaID
348+
for _, name := range c.Names {
349+
idToAnchor[name] = uid
350+
}
351+
}
352+
for _, v := range pi.doc.Vars {
353+
commaID := strings.Join(v.Names, ",")
354+
uid := pi.doc.ImportPath + "." + commaID
355+
for _, name := range v.Names {
356+
idToAnchor[name] = uid
357+
}
358+
}
359+
for _, f := range pi.doc.Funcs {
360+
uid := pi.doc.ImportPath + "." + f.Name
361+
idToAnchor[f.Name] = uid
362+
}
363+
for _, t := range pi.doc.Types {
364+
uid := pi.doc.ImportPath + "." + t.Name
365+
idToAnchor[t.Name] = uid
366+
for _, c := range t.Consts {
367+
commaID := strings.Join(c.Names, ",")
368+
uid := pi.doc.ImportPath + "." + commaID
369+
for _, name := range c.Names {
370+
idToAnchor[name] = uid
371+
}
372+
}
373+
for _, v := range t.Vars {
374+
commaID := strings.Join(v.Names, ",")
375+
uid := pi.doc.ImportPath + "." + commaID
376+
for _, name := range v.Names {
377+
idToAnchor[name] = uid
378+
}
379+
}
380+
for _, f := range t.Funcs {
381+
uid := pi.doc.ImportPath + "." + t.Name + "." + f.Name
382+
idToAnchor[f.Name] = uid
383+
}
384+
for _, m := range t.Methods {
385+
uid := pi.doc.ImportPath + "." + t.Name + "." + m.Name
386+
idToAnchor[m.Name] = uid
387+
}
388+
}
389+
390+
for id, anchor := range idToAnchor {
391+
idToAnchor[id] = nonWordRegex.ReplaceAllString(anchor, "_")
392+
}
393+
394+
return idToAnchor
328395
}
329396

330397
func (l *linker) linkify(s string) string {
@@ -342,11 +409,11 @@ func (l *linker) linkify(s string) string {
342409
// If s is not exported, it's probably a builtin.
343410
if !token.IsExported(s) {
344411
if doc.IsPredeclared(s) {
345-
return href(toURL("builtin", s), s)
412+
return href(l.toURL("builtin", s), s)
346413
}
347414
return fmt.Sprintf("%s%s", prefix, s)
348415
}
349-
return fmt.Sprintf("%s%s", prefix, href(toURL("", s), s))
416+
return fmt.Sprintf("%s%s", prefix, href(l.toURL("", s), s))
350417
}
351418
// Otherwise, it's in another package.
352419
split := strings.Split(s, ".")
@@ -362,14 +429,17 @@ func (l *linker) linkify(s string) string {
362429
return fmt.Sprintf("%s%s", prefix, s)
363430
}
364431
name := split[1]
365-
return fmt.Sprintf("%s%s.%s", prefix, href(toURL(pkgPath, ""), pkg), href(toURL(pkgPath, name), name))
432+
return fmt.Sprintf("%s%s.%s", prefix, href(l.toURL(pkgPath, ""), pkg), href(l.toURL(pkgPath, name), name))
366433
}
367434

368435
// TODO: link to the right baseURL, with the right module name and version
369436
// pattern.
370-
func toURL(pkg, name string) string {
437+
func (l *linker) toURL(pkg, name string) string {
371438
if pkg == "" {
372-
return fmt.Sprintf("#%s", strings.ToLower(name))
439+
if anchor := l.idToAnchor[name]; anchor != "" {
440+
name = anchor
441+
}
442+
return fmt.Sprintf("#%s", name)
373443
}
374444
baseURL := "https://pkg.go.dev"
375445
if name == "" {

0 commit comments

Comments
 (0)