cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / scripts / doccheck / tree.go
tree.go 5.7K
1package main23import (4 "encoding/json"5 "fmt"6 "io/fs"7 "os"8 "path"9 "path/filepath"10 "regexp"11 "strconv"12 "strings"13)1415// The docs are a Docusaurus tree that also reads on GitHub: each folder16// under docs/ has a README.md that indexes it and a _category_.json that17// names it in the sidebar, and each page has front matter with its title18// and sidebar position.1920// checkTree reports pages their folder's README.md doesn't link, folders21// their parent's README.md doesn't link, and missing or invalid category22// files and front matter.23func checkTree(docs map[string]*doc) []string {24 var out []string25 folders := map[string]bool{"docs": true}26 for p := range docs {27 if strings.HasPrefix(p, "docs/") {28 folders[path.Dir(p)] = true29 }30 }31 for _, dir := range sortedKeys(folders) {32 index := dir + "/README.md"33 if docs[index] == nil {34 out = append(out, index+": missing; every folder of docs has one")35 continue36 }37 linked := linkedFrom(docs[index])38 for _, p := range sortedKeys(docs) {39 if p != index && path.Dir(p) == dir && !linked[p] {40 out = append(out, index+": doesn't link "+p)41 }42 }43 for sub := range folders {44 if path.Dir(sub) == dir && sub != dir && !linked[sub+"/README.md"] {45 out = append(out, index+": doesn't link "+sub+"/README.md")46 }47 }48 if dir != "docs" {49 out = append(out, checkCategory(dir, folders)...)50 }51 }52 out = append(out, checkFrontMatter(docs)...)53 return out54}5556func linkedFrom(dc *doc) map[string]bool {57 linked := map[string]bool{}58 for _, l := range dc.links {59 file, _, _ := strings.Cut(l.target, "#")60 if file != "" && !strings.Contains(file, "://") {61 linked[path.Clean(path.Join(path.Dir(dc.path), file))] = true62 }63 }64 return linked65}6667// category is a folder's _category_.json, as Docusaurus reads it.68type category struct {69 Label string `json:"label"`70 Position *int `json:"position"`71 Link *struct {72 Type string `json:"type"`73 ID string `json:"id"`74 } `json:"link"`75}7677// checkCategory reports a folder's missing or invalid _category_.json,78// and positions its siblings share.79func checkCategory(dir string, folders map[string]bool) []string {80 file := dir + "/_category_.json"81 c, err := readCategory(file)82 if err != nil {83 return []string{file + ": " + err.Error()}84 }85 var out []string86 if c.Label == "" || c.Position == nil {87 out = append(out, file+": needs a label and a position")88 }89 if c.Link != nil && c.Link.ID != strings.TrimPrefix(dir, "docs/")+"/README" {90 out = append(out, fmt.Sprintf("%s: link id %q isn't the folder's README", file, c.Link.ID))91 }92 for sib := range folders {93 if sib == dir || path.Dir(sib) != path.Dir(dir) || sib == "docs" || c.Position == nil {94 continue95 }96 if o, err := readCategory(sib + "/_category_.json"); err == nil && o.Position != nil && *o.Position == *c.Position && sib < dir {97 out = append(out, fmt.Sprintf("%s: position %d is also %s's", file, *c.Position, sib))98 }99 }100 return out101}102103func readCategory(file string) (category, error) {104 var c category105 data, err := os.ReadFile(file)106 if err != nil {107 return c, fmt.Errorf("missing; every folder of docs has one")108 }109 if err := json.Unmarshal(data, &c); err != nil {110 return c, fmt.Errorf("not valid JSON: %v", err)111 }112 return c, nil113}114115var frontMatter = regexp.MustCompile(`\A---\n((?:.*\n)*?)---\n`)116117// checkFrontMatter reports pages under docs/ without a title and a118// sidebar position, and positions pages of one folder share.119func checkFrontMatter(docs map[string]*doc) []string {120 var out []string121 seen := map[string]string{} // folder and position -> page122 for _, p := range sortedKeys(docs) {123 if !strings.HasPrefix(p, "docs/") {124 continue125 }126 data, err := os.ReadFile(filepath.FromSlash(p))127 if err != nil {128 continue129 }130 m := frontMatter.FindSubmatch(data)131 if m == nil {132 out = append(out, p+": no front matter (title, sidebar_position)")133 continue134 }135 fields := map[string]string{}136 for l := range strings.SplitSeq(string(m[1]), "\n") {137 if k, v, ok := strings.Cut(l, ":"); ok {138 fields[strings.TrimSpace(k)] = strings.TrimSpace(v)139 }140 }141 pos, err := strconv.Atoi(fields["sidebar_position"])142 if fields["title"] == "" || err != nil {143 out = append(out, p+": front matter needs a title and a whole-number sidebar_position")144 continue145 }146 key := fmt.Sprintf("%s#%d", path.Dir(p), pos)147 if other, ok := seen[key]; ok {148 out = append(out, fmt.Sprintf("%s: sidebar_position %d is also %s's", p, pos, other))149 }150 seen[key] = p151 }152 return out153}154155var goDocPath = regexp.MustCompile(`\bdocs/[a-z0-9_/-]+\.md\b`)156157// checkGoPaths reports doc paths named outside the docs, in Go code158// (messages and comments), the Makefile, scripts and deploy configs,159// that don't exist.160func checkGoPaths() []string {161 var out []string162 for _, f := range []string{"Makefile", "CLAUDE.md"} {163 out = append(out, missingDocPaths(f)...)164 }165 for _, root := range []string{"cmd", "internal", "e2e", "oracle", "demos", "scripts", "deploy"} {166 filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {167 if err == nil && !d.IsDir() && pathBearing[filepath.Ext(p)] {168 out = append(out, missingDocPaths(p)...)169 }170 return nil171 })172 }173 return out174}175176// pathBearing are the kinds of file outside docs/ that name doc pages.177var pathBearing = map[string]bool{".go": true, ".sh": true, ".yml": true, ".yaml": true, ".tape": true, ".sql": true, ".json": true}178179// missingDocPaths reports the doc paths file p names that don't exist.180func missingDocPaths(p string) []string {181 data, err := os.ReadFile(p)182 if err != nil {183 return nil184 }185 var out []string186 for i, l := range strings.Split(string(data), "\n") {187 for _, m := range goDocPath.FindAllString(l, -1) {188 if _, err := os.Stat(filepath.FromSlash(m)); err != nil {189 out = append(out, fmt.Sprintf("%s:%d: %s: no such doc", p, i+1, m))190 }191 }192 }193 return out194}