// Package breadcrumbs generates metadata required to enable breadcrumb // navigation. This is particularly helpful for sites that have deep // hierarchies which may be otherwise confusing to visitors. package breadcrumbs import ( "fmt" "sync" "git.foosoft.net/alex/goldsmith" "git.foosoft.net/alex/goldsmith/filters/wildcard" ) // Crumb provides organizational information about this node and ones before it. type Crumb struct { Ancestors []*Node Node *Node } // Node represents information about a specific file in the site's structure. type Node struct { File *goldsmith.File Parent *Node Children []*Node parentName string } // Breadcrumbs chainable plugin context. type Breadcrumbs struct { nameKey string parentKey string crumbsKey string allNodes []*Node namedNodes map[string]*Node mutex sync.Mutex } // New creates a new instance of the Breadcrumbs plugin. func New() *Breadcrumbs { return &Breadcrumbs{ nameKey: "CrumbName", parentKey: "CrumbParent", crumbsKey: "Crumbs", namedNodes: make(map[string]*Node), } } // NameKey sets the metadata key used to access the crumb name (default: "CrumbName"). // Crumb names must be globally unique within any given website. func (self *Breadcrumbs) NameKey(key string) *Breadcrumbs { self.nameKey = key return self } // ParentKey sets the metadata key used to access the parent name (default: "CrumbParent"). func (self *Breadcrumbs) ParentKey(key string) *Breadcrumbs { self.parentKey = key return self } // CrumbsKey sets the metadata key used to store information about crumbs (default: "Crumbs"). func (self *Breadcrumbs) CrumbsKey(key string) *Breadcrumbs { self.crumbsKey = key return self } func (*Breadcrumbs) Name() string { return "breadcrumbs" } func (*Breadcrumbs) Initialize(context *goldsmith.Context) error { context.Filter(wildcard.New("**/*.html", "**/*.htm")) return nil } func (self *Breadcrumbs) Process(context *goldsmith.Context, inputFile *goldsmith.File) error { var parentNameStr string if parentName, ok := inputFile.Prop(self.parentKey); ok { parentNameStr, _ = parentName.(string) } var nodeNameStr string if nodeName, ok := inputFile.Prop(self.nameKey); ok { nodeNameStr, _ = nodeName.(string) } self.mutex.Lock() defer self.mutex.Unlock() node := &Node{File: inputFile, parentName: parentNameStr} self.allNodes = append(self.allNodes, node) if len(nodeNameStr) > 0 { if _, ok := self.namedNodes[nodeNameStr]; ok { return fmt.Errorf("duplicate node: %s", nodeNameStr) } self.namedNodes[nodeNameStr] = node } return nil } func (self *Breadcrumbs) Finalize(context *goldsmith.Context) error { for _, node := range self.allNodes { if len(node.parentName) == 0 { continue } if parentNode, ok := self.namedNodes[node.parentName]; ok { parentNode.Children = append(parentNode.Children, node) node.Parent = parentNode } else { return fmt.Errorf("undefined parent: %s", node.parentName) } } for _, node := range self.allNodes { var ancestors []*Node for currentNode := node.Parent; currentNode != nil; currentNode = currentNode.Parent { ancestors = append([]*Node{currentNode}, ancestors...) } node.File.SetProp(self.crumbsKey, Crumb{ancestors, node}) context.DispatchFile(node.File) } return nil }