// Package tags builds tag clouds from file metadata. This makes it easy to // create lists of all files tagged with a specific tag, as well as to see all // tags globally used on a site. package tags import ( "bytes" "path" "sort" "strings" "sync" "unicode" "git.foosoft.net/alex/goldsmith" "git.foosoft.net/alex/goldsmith/filters/wildcard" ) // TagInfo contains site-wide information about a particular tag. type TagInfo struct { TaggedFiles filesByPath IndexFile *goldsmith.File SafeName string RawName string } // TagState contains site-wide information about tags used on a site. type TagState struct { CurrentTag *TagInfo CurrentTags tagInfoByName TagsByName *tagInfoByName TagsByCount *tagInfoByCount } // Tags chainable context. type Tags struct { tagsKey string stateKey string baseDir string indexName string indexMeta map[string]interface{} info map[string]*TagInfo infoByName tagInfoByName infoByCount tagInfoByCount files []*goldsmith.File mutex sync.Mutex } // New creates a new instance of the Tags plugin. func New() *Tags { return &Tags{ tagsKey: "Tags", stateKey: "TagState", baseDir: "tags", indexName: "index.html", info: make(map[string]*TagInfo), } } // TagsKey sets the metadata key used to get the tags for this file, stored as a slice of strings (default: "Tags"). func (self *Tags) TagsKey(key string) *Tags { self.tagsKey = key return self } // StateKey sets the meatadata key used to store site-wide tag information (default: "TagState"). func (self *Tags) StateKey(key string) *Tags { self.stateKey = key return self } // IndexName sets the filename which will be used to create tag list files (default: "index.html"). func (plugin *Tags) IndexName(name string) *Tags { plugin.indexName = name return plugin } // IndexMeta sets the metadata which will be assigned to generated tag list files (default: {}). func (self *Tags) IndexMeta(meta map[string]interface{}) *Tags { self.indexMeta = meta return self } // BaseDir sets the base directory used to generate tag list files (default: "tags"). func (self *Tags) BaseDir(dir string) *Tags { self.baseDir = dir return self } func (*Tags) Name() string { return "tags" } func (*Tags) Initialize(context *goldsmith.Context) error { context.Filter(wildcard.New("**/*.html", "**/*.htm")) return nil } func (self *Tags) Process(context *goldsmith.Context, inputFile *goldsmith.File) error { tagState := &TagState{ TagsByName: &self.infoByName, TagsByCount: &self.infoByCount, } self.mutex.Lock() defer func() { inputFile.SetProp(self.stateKey, tagState) self.files = append(self.files, inputFile) self.mutex.Unlock() }() tagsArr, ok := inputFile.Props()[self.tagsKey].([]interface{}) if !ok { return nil } for _, tag := range tagsArr { tagRaw, ok := tag.(string) if !ok { continue } tagSafe := safeTag(tagRaw) if len(tagSafe) == 0 { continue } var duplicate bool for _, tagState := range tagState.CurrentTags { if tagState.RawName == tagRaw { duplicate = true break } } if duplicate { continue } info, ok := self.info[tagRaw] if !ok { info = &TagInfo{ SafeName: tagSafe, RawName: tagRaw, } self.info[tagRaw] = info } info.TaggedFiles = append(info.TaggedFiles, inputFile) tagState.CurrentTags = append(tagState.CurrentTags, info) } sort.Sort(tagState.CurrentTags) return nil } func (self *Tags) Finalize(context *goldsmith.Context) error { for _, info := range self.info { sort.Sort(info.TaggedFiles) self.infoByName = append(self.infoByName, info) self.infoByCount = append(self.infoByCount, info) } sort.Sort(self.infoByName) sort.Sort(self.infoByCount) if self.indexMeta != nil { files, err := self.buildPages(context) if err != nil { return err } self.files = append(self.files, files...) } for _, file := range self.files { context.DispatchFile(file) } return nil } func (self *Tags) buildPages(context *goldsmith.Context) ([]*goldsmith.File, error) { var files []*goldsmith.File for tag, info := range self.info { var err error info.IndexFile, err = context.CreateFileFromReader(self.tagPagePath(tag), bytes.NewReader(nil)) if err != nil { return nil, err } info.IndexFile.SetProp(self.stateKey, &TagState{ CurrentTag: info, TagsByName: &self.infoByName, TagsByCount: &self.infoByCount, }) for name, value := range self.indexMeta { info.IndexFile.SetProp(name, value) } files = append(files, info.IndexFile) } return files, nil } func (self *Tags) tagPagePath(tag string) string { return path.Join(self.baseDir, safeTag(tag), self.indexName) } func safeTag(tagRaw string) string { tagRaw = strings.TrimSpace(tagRaw) tagRaw = strings.ToLower(tagRaw) var valid bool var tagSafe string for _, c := range tagRaw { if unicode.IsLetter(c) || unicode.IsNumber(c) { tagSafe += string(c) valid = true } else if valid { tagSafe += "-" valid = false } } return tagSafe } type tagInfoByCount []*TagInfo func (self tagInfoByCount) Len() int { return len(self) } func (self tagInfoByCount) Swap(i, j int) { self[i], self[j] = self[j], self[i] } func (self tagInfoByCount) Less(i, j int) bool { if len(self[i].TaggedFiles) > len(self[j].TaggedFiles) { return true } else if len(self[i].TaggedFiles) == len(self[j].TaggedFiles) && strings.Compare(self[i].RawName, self[j].RawName) < 0 { return true } return false } type tagInfoByName []*TagInfo func (self tagInfoByName) Len() int { return len(self) } func (self tagInfoByName) Swap(i, j int) { self[i], self[j] = self[j], self[i] } func (self tagInfoByName) Less(i, j int) bool { if strings.Compare(self[i].RawName, self[j].RawName) < 0 { return true } else if self[i].RawName == self[j].RawName && len(self[i].TaggedFiles) > len(self[j].TaggedFiles) { return true } return false } type filesByPath []*goldsmith.File func (self filesByPath) Len() int { return len(self) } func (self filesByPath) Swap(i, j int) { self[i], self[j] = self[j], self[i] } func (self filesByPath) Less(i, j int) bool { return strings.Compare(self[i].Path(), self[j].Path()) < 0 }