Skip to content

Commit 3a49b2d

Browse files
authored
feat(godocfx): add nesting to TOC (#2972)
1 parent 276ec88 commit 3a49b2d

File tree

3 files changed

+149
-29
lines changed

3 files changed

+149
-29
lines changed

internal/godocfx/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.15
44

55
require (
66
cloud.google.com/go v0.67.0
7+
cloud.google.com/go/bigquery v1.8.0
78
cloud.google.com/go/storage v1.11.0
89
github.com/kr/pretty v0.2.1 // indirect
910
golang.org/x/tools v0.0.0-20201005185003-576e169c3de7

internal/godocfx/godocfx_test.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ package main
1818

1919
import (
2020
"flag"
21+
"fmt"
2122
"io/ioutil"
2223
"os"
2324
"path/filepath"
2425
"testing"
2526

26-
_ "cloud.google.com/go/storage" // Implicitly required by test.
27+
_ "cloud.google.com/go/bigquery" // Implicitly required by test.
28+
_ "cloud.google.com/go/storage" // Implicitly required by test.
2729
)
2830

2931
var updateGoldens bool
@@ -35,22 +37,22 @@ func TestMain(m *testing.M) {
3537
}
3638

3739
func TestParse(t *testing.T) {
38-
testPath := "cloud.google.com/go/storage"
39-
r, err := parse(testPath, nil)
40+
mod := "cloud.google.com/go/bigquery"
41+
r, err := parse(mod+"/...", []string{"README.md"})
4042
if err != nil {
4143
t.Fatalf("Parse: %v", err)
4244
}
4345
if got, want := len(r.toc), 1; got != want {
4446
t.Fatalf("Parse got len(toc) = %d, want %d", got, want)
4547
}
46-
if got, want := len(r.pages), 1; got != want {
48+
if got, want := len(r.pages), 10; got != want {
4749
t.Errorf("Parse got len(pages) = %d, want %d", got, want)
4850
}
49-
if got := r.module.Path; got != testPath {
50-
t.Fatalf("Parse got module = %q, want %q", got, testPath)
51+
if got := r.module.Path; got != mod {
52+
t.Fatalf("Parse got module = %q, want %q", got, mod)
5153
}
5254

53-
page := r.pages[testPath]
55+
page := r.pages[mod]
5456

5557
// Check invariants for every item.
5658
for _, item := range page.Items {
@@ -63,7 +65,7 @@ func TestParse(t *testing.T) {
6365
}
6466
}
6567

66-
// Check there is at least one type, const, variable, and function.
68+
// Check there is at least one type, const, variable, function, and method.
6769
wants := []string{"type", "const", "variable", "function", "method"}
6870
for _, want := range wants {
6971
found := false
@@ -77,6 +79,31 @@ func TestParse(t *testing.T) {
7779
t.Errorf("Parse got no %q, want at least one", want)
7880
}
7981
}
82+
83+
foundREADME := false
84+
foundNested := false
85+
foundUnnested := false
86+
for _, item := range r.toc[0].Items {
87+
fmt.Println(item.Name)
88+
if item.Name == "README" {
89+
foundREADME = true
90+
}
91+
if len(item.Items) > 0 {
92+
foundNested = true
93+
}
94+
if len(item.Items) == 0 && len(item.UID) > 0 && len(item.Name) > 0 {
95+
foundUnnested = true
96+
}
97+
}
98+
if !foundREADME {
99+
t.Errorf("Parse didn't find a README in TOC")
100+
}
101+
if !foundNested {
102+
t.Errorf("Parse didn't find a nested element in TOC")
103+
}
104+
if !foundUnnested {
105+
t.Errorf("Parse didn't find an unnested element in TOC (e.g. datatransfer/apiv1)")
106+
}
80107
}
81108

82109
func TestGoldens(t *testing.T) {

internal/godocfx/parse.go

Lines changed: 113 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ func parse(glob string, extraFiles []string) (*result, error) {
123123
}
124124

125125
pages := map[string]*page{}
126-
toc := tableOfContents{}
127126

128127
module := pkgs[0].Module
129128
skippedModules := map[string]struct{}{}
@@ -177,6 +176,8 @@ func parse(glob string, extraFiles []string) (*result, error) {
177176
}
178177
sort.Strings(pkgNames)
179178

179+
toc := buildTOC(module.Path, pkgNames, extraFiles)
180+
180181
// Once the files are grouped by package, process each package
181182
// independently.
182183
for _, pkgPath := range pkgNames {
@@ -201,26 +202,6 @@ func parse(glob string, extraFiles []string) (*result, error) {
201202
continue
202203
}
203204

204-
pkgTOCITem := &tocItem{
205-
UID: docPkg.ImportPath,
206-
Name: docPkg.ImportPath,
207-
}
208-
toc = append(toc, pkgTOCITem)
209-
210-
// If the package path == module path, add the extra files to the TOC
211-
// as a child of the module==pkg item.
212-
if pkgPath == module.Path {
213-
for _, path := range extraFiles {
214-
base := filepath.Base(path)
215-
name := strings.TrimSuffix(base, filepath.Ext(base))
216-
name = strings.Title(name)
217-
pkgTOCITem.addItem(&tocItem{
218-
Href: path,
219-
Name: name,
220-
})
221-
}
222-
}
223-
224205
pkgItem := &item{
225206
UID: docPkg.ImportPath,
226207
Name: docPkg.ImportPath,
@@ -412,3 +393,114 @@ func processExamples(exs []*doc.Example, fset *token.FileSet) []example {
412393
}
413394
return result
414395
}
396+
397+
func buildTOC(mod string, pkgs []string, extraFiles []string) tableOfContents {
398+
toc := tableOfContents{}
399+
400+
modTOC := &tocItem{
401+
UID: mod, // Assume the module root has a package.
402+
Name: mod,
403+
}
404+
for _, path := range extraFiles {
405+
base := filepath.Base(path)
406+
name := strings.TrimSuffix(base, filepath.Ext(base))
407+
name = strings.Title(name)
408+
modTOC.addItem(&tocItem{
409+
Href: path,
410+
Name: name,
411+
})
412+
}
413+
414+
toc = append(toc, modTOC)
415+
416+
if len(pkgs) == 1 {
417+
// The module only has one package.
418+
return toc
419+
}
420+
421+
// partialTrie represents the parts of each package name that come after
422+
// the module name.
423+
//
424+
// A package name is three parts: mod/mid/suffix
425+
//
426+
// For example, cloud.google.com/go/bigquery/connection/apiv1 would be split
427+
// into cloud.google.com/go, bigquery, and connection/apiv1.
428+
//
429+
// Don't do a full trie to keep nesting to a minimum.
430+
partialTrie := map[string][]string{}
431+
mids := []string{}
432+
433+
for _, pkg := range pkgs {
434+
if pkg == mod {
435+
continue
436+
}
437+
if !strings.HasPrefix(pkg, mod) {
438+
panic(fmt.Sprintf("Package %q does not start with %q, should never happen", pkg, mod))
439+
}
440+
midAndSuffix := strings.TrimPrefix(pkg, mod+"/")
441+
parts := strings.SplitN(midAndSuffix, "/", 2)
442+
443+
mid := parts[0]
444+
suffix := ""
445+
if len(parts) > 1 {
446+
suffix = parts[1]
447+
}
448+
449+
if _, ok := partialTrie[mid]; !ok {
450+
mids = append(mids, mid)
451+
}
452+
453+
partialTrie[mid] = append(partialTrie[mid], suffix)
454+
}
455+
456+
sort.Strings(mids)
457+
458+
for _, mid := range mids {
459+
suffixes := partialTrie[mid]
460+
461+
// No need to nest if there is only one suffix.
462+
if len(suffixes) == 1 {
463+
suffix := suffixes[0]
464+
name := mid
465+
if suffix != "" {
466+
name = name + "/" + suffix
467+
}
468+
uid := mod + "/" + name
469+
pkgTOCItem := &tocItem{
470+
UID: uid,
471+
Name: name,
472+
}
473+
modTOC.addItem(pkgTOCItem)
474+
continue
475+
}
476+
477+
sort.Strings(suffixes)
478+
479+
midTOC := &tocItem{
480+
// No Uref or UID because this may not be a package.
481+
Name: mid,
482+
}
483+
modTOC.addItem(midTOC)
484+
485+
for _, suffix := range suffixes {
486+
uid := mod + "/" + mid
487+
if suffix != "" {
488+
uid = uid + "/" + suffix
489+
}
490+
491+
// Empty suffix means this mid is a package itself.
492+
if suffix == "" {
493+
midTOC.UID = uid
494+
continue
495+
}
496+
497+
pkgTOC := &tocItem{
498+
UID: uid,
499+
Name: suffix,
500+
}
501+
midTOC.addItem(pkgTOC)
502+
}
503+
}
504+
505+
return toc
506+
}

0 commit comments

Comments
 (0)