github.com/whatlly/hugo@v0.47.1/hugolib/pages_related.go (about) 1 // Copyright 2017-present The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "sync" 18 19 "github.com/gohugoio/hugo/common/types" 20 "github.com/gohugoio/hugo/related" 21 "github.com/spf13/cast" 22 ) 23 24 var ( 25 // Assert that Pages and PageGroup implements the PageGenealogist interface. 26 _ PageGenealogist = (Pages)(nil) 27 _ PageGenealogist = PageGroup{} 28 ) 29 30 // A PageGenealogist finds related pages in a page collection. This interface is implemented 31 // by Pages and PageGroup, which makes it available as `{{ .RegularPages.Related . }}` etc. 32 type PageGenealogist interface { 33 34 // Template example: 35 // {{ $related := .RegularPages.Related . }} 36 Related(doc related.Document) (Pages, error) 37 38 // Template example: 39 // {{ $related := .RegularPages.RelatedIndices . "tags" "date" }} 40 RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) 41 42 // Template example: 43 // {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks") ( keyVals "date" .Date ) }} 44 RelatedTo(args ...types.KeyValues) (Pages, error) 45 } 46 47 // Related searches all the configured indices with the search keywords from the 48 // supplied document. 49 func (p Pages) Related(doc related.Document) (Pages, error) { 50 page, err := unwrapPage(doc) 51 if err != nil { 52 return nil, err 53 } 54 55 result, err := p.searchDoc(page) 56 if err != nil { 57 return nil, err 58 } 59 60 return result.removeFirstIfFound(page), nil 61 } 62 63 // RelatedIndices searches the given indices with the search keywords from the 64 // supplied document. 65 func (p Pages) RelatedIndices(doc related.Document, indices ...interface{}) (Pages, error) { 66 page, err := unwrapPage(doc) 67 if err != nil { 68 return nil, err 69 } 70 71 indicesStr, err := cast.ToStringSliceE(indices) 72 if err != nil { 73 return nil, err 74 } 75 76 result, err := p.searchDoc(doc, indicesStr...) 77 if err != nil { 78 return nil, err 79 } 80 81 return result.removeFirstIfFound(page), nil 82 83 } 84 85 // RelatedTo searches the given indices with the corresponding values. 86 func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) { 87 if len(p) == 0 { 88 return nil, nil 89 } 90 91 return p.search(args...) 92 93 } 94 95 func (p Pages) search(args ...types.KeyValues) (Pages, error) { 96 return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { 97 return idx.SearchKeyValues(args...) 98 }) 99 100 } 101 102 func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) { 103 return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { 104 return idx.SearchDoc(doc, indices...) 105 }) 106 } 107 108 func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) { 109 if len(p) == 0 { 110 return nil, nil 111 } 112 113 cache := p[0].s.relatedDocsHandler 114 115 searchIndex, err := cache.getOrCreateIndex(p) 116 if err != nil { 117 return nil, err 118 } 119 120 result, err := search(searchIndex) 121 if err != nil { 122 return nil, err 123 } 124 125 if len(result) > 0 { 126 mp := make(Pages, len(result)) 127 for i, match := range result { 128 mp[i] = match.(*Page) 129 } 130 return mp, nil 131 } 132 133 return nil, nil 134 } 135 136 type cachedPostingList struct { 137 p Pages 138 139 postingList *related.InvertedIndex 140 } 141 142 type relatedDocsHandler struct { 143 // This is configured in site or langugage config. 144 cfg related.Config 145 146 postingLists []*cachedPostingList 147 mu sync.RWMutex 148 } 149 150 func newSearchIndexHandler(cfg related.Config) *relatedDocsHandler { 151 return &relatedDocsHandler{cfg: cfg} 152 } 153 154 // This assumes that a lock has been acquired. 155 func (s *relatedDocsHandler) getIndex(p Pages) *related.InvertedIndex { 156 for _, ci := range s.postingLists { 157 if fastEqualPages(p, ci.p) { 158 return ci.postingList 159 } 160 } 161 return nil 162 } 163 164 func (s *relatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) { 165 s.mu.RLock() 166 cachedIndex := s.getIndex(p) 167 if cachedIndex != nil { 168 s.mu.RUnlock() 169 return cachedIndex, nil 170 } 171 s.mu.RUnlock() 172 173 s.mu.Lock() 174 defer s.mu.Unlock() 175 176 if cachedIndex := s.getIndex(p); cachedIndex != nil { 177 return cachedIndex, nil 178 } 179 180 searchIndex := related.NewInvertedIndex(s.cfg) 181 182 for _, page := range p { 183 if err := searchIndex.Add(page); err != nil { 184 return nil, err 185 } 186 } 187 188 s.postingLists = append(s.postingLists, &cachedPostingList{p: p, postingList: searchIndex}) 189 190 return searchIndex, nil 191 }