package content import ( "os" "path/filepath" "sort" "strings" ) type Node struct { Title string Path string // URL path relative to content root FilePath string // absolute filesystem path Children []*Node IsDir bool } func BuildTree(root string) (*Node, error) { tree := &Node{Title: "root", IsDir: true} if err := buildDir(root, root, tree); err != nil { return nil, err } return tree, nil } func buildDir(base, dir string, parent *Node) error { entries, err := os.ReadDir(dir) if err != nil { return err } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) for _, e := range entries { name := e.Name() full := filepath.Join(dir, name) if strings.HasPrefix(name, ".") || name == "book.yaml" { continue } rel, _ := filepath.Rel(base, full) urlPath := filepath.ToSlash(rel) if e.IsDir() { node := &Node{ Title: displayName(name), Path: urlPath, FilePath: full, IsDir: true, } if err := buildDir(base, full, node); err != nil { return err } if len(node.Children) > 0 || hasIndex(full) { parent.Children = append(parent.Children, node) } } else if strings.HasSuffix(name, ".md") { urlPath = strings.TrimSuffix(urlPath, ".md") node := &Node{ Title: displayName(strings.TrimSuffix(name, ".md")), Path: urlPath, FilePath: full, } parent.Children = append(parent.Children, node) } } return nil } func hasIndex(dir string) bool { _, err := os.Stat(filepath.Join(dir, "_index.md")) return err == nil } func displayName(name string) string { if name == "_index" { return "Overview" } if len(name) > 3 && name[2] == '-' && name[0] >= '0' && name[0] <= '9' && name[1] >= '0' && name[1] <= '9' { name = name[3:] } name = strings.ReplaceAll(name, "-", " ") if len(name) > 0 { return strings.ToUpper(name[:1]) + name[1:] } return name } func (n *Node) Flatten() []*Node { var out []*Node n.flatten(&out) return out } func (n *Node) flatten(out *[]*Node) { if !n.IsDir { *out = append(*out, n) } for _, c := range n.Children { c.flatten(out) } } func (n *Node) FindByPath(path string) *Node { if !n.IsDir && n.Path == path { return n } for _, c := range n.Children { if found := c.FindByPath(path); found != nil { return found } } return nil } func (n *Node) FindDirByPath(path string) *Node { if n.IsDir && n.Path == path { return n } for _, c := range n.Children { if found := c.FindDirByPath(path); found != nil { return found } } return nil } func (n *Node) IndexPath() string { if n.IsDir { return filepath.Join(n.FilePath, "_index.md") } return n.FilePath } func (n *Node) FirstPage() *Node { pages := n.Flatten() if len(pages) > 0 { return pages[0] } return nil }