pages_related.go (4899B)
1 // Copyright 2019 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 page 15 16 import ( 17 "fmt" 18 "sync" 19 20 "github.com/gohugoio/hugo/common/types" 21 "github.com/gohugoio/hugo/related" 22 "github.com/spf13/cast" 23 ) 24 25 var ( 26 // Assert that Pages and PageGroup implements the PageGenealogist interface. 27 _ PageGenealogist = (Pages)(nil) 28 _ PageGenealogist = PageGroup{} 29 ) 30 31 // A PageGenealogist finds related pages in a page collection. This interface is implemented 32 // by Pages and PageGroup, which makes it available as `{{ .RegularRelated . }}` etc. 33 type PageGenealogist interface { 34 35 // Template example: 36 // {{ $related := .RegularPages.Related . }} 37 Related(doc related.Document) (Pages, error) 38 39 // Template example: 40 // {{ $related := .RegularPages.RelatedIndices . "tags" "date" }} 41 RelatedIndices(doc related.Document, indices ...any) (Pages, error) 42 43 // Template example: 44 // {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks") ( keyVals "date" .Date ) }} 45 RelatedTo(args ...types.KeyValues) (Pages, error) 46 } 47 48 // Related searches all the configured indices with the search keywords from the 49 // supplied document. 50 func (p Pages) Related(doc related.Document) (Pages, error) { 51 result, err := p.searchDoc(doc) 52 if err != nil { 53 return nil, err 54 } 55 56 if page, ok := doc.(Page); ok { 57 return result.removeFirstIfFound(page), nil 58 } 59 60 return result, 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 ...any) (Pages, error) { 66 indicesStr, err := cast.ToStringSliceE(indices) 67 if err != nil { 68 return nil, err 69 } 70 71 result, err := p.searchDoc(doc, indicesStr...) 72 if err != nil { 73 return nil, err 74 } 75 76 if page, ok := doc.(Page); ok { 77 return result.removeFirstIfFound(page), nil 78 } 79 80 return result, nil 81 } 82 83 // RelatedTo searches the given indices with the corresponding values. 84 func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) { 85 if len(p) == 0 { 86 return nil, nil 87 } 88 89 return p.search(args...) 90 } 91 92 func (p Pages) search(args ...types.KeyValues) (Pages, error) { 93 return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { 94 return idx.SearchKeyValues(args...) 95 }) 96 } 97 98 func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) { 99 return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { 100 return idx.SearchDoc(doc, indices...) 101 }) 102 } 103 104 func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) { 105 if len(p) == 0 { 106 return nil, nil 107 } 108 109 d, ok := p[0].(InternalDependencies) 110 if !ok { 111 return nil, fmt.Errorf("invalid type %T in related search", p[0]) 112 } 113 114 cache := d.GetRelatedDocsHandler() 115 116 searchIndex, err := cache.getOrCreateIndex(p) 117 if err != nil { 118 return nil, err 119 } 120 121 result, err := search(searchIndex) 122 if err != nil { 123 return nil, err 124 } 125 126 if len(result) > 0 { 127 mp := make(Pages, len(result)) 128 for i, match := range result { 129 mp[i] = match.(Page) 130 } 131 return mp, nil 132 } 133 134 return nil, nil 135 } 136 137 type cachedPostingList struct { 138 p Pages 139 140 postingList *related.InvertedIndex 141 } 142 143 type RelatedDocsHandler struct { 144 cfg related.Config 145 146 postingLists []*cachedPostingList 147 mu sync.RWMutex 148 } 149 150 func NewRelatedDocsHandler(cfg related.Config) *RelatedDocsHandler { 151 return &RelatedDocsHandler{cfg: cfg} 152 } 153 154 func (s *RelatedDocsHandler) Clone() *RelatedDocsHandler { 155 return NewRelatedDocsHandler(s.cfg) 156 } 157 158 // This assumes that a lock has been acquired. 159 func (s *RelatedDocsHandler) getIndex(p Pages) *related.InvertedIndex { 160 for _, ci := range s.postingLists { 161 if pagesEqual(p, ci.p) { 162 return ci.postingList 163 } 164 } 165 return nil 166 } 167 168 func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) { 169 s.mu.RLock() 170 cachedIndex := s.getIndex(p) 171 if cachedIndex != nil { 172 s.mu.RUnlock() 173 return cachedIndex, nil 174 } 175 s.mu.RUnlock() 176 177 s.mu.Lock() 178 defer s.mu.Unlock() 179 180 if cachedIndex := s.getIndex(p); cachedIndex != nil { 181 return cachedIndex, nil 182 } 183 184 searchIndex := related.NewInvertedIndex(s.cfg) 185 186 for _, page := range p { 187 if err := searchIndex.Add(page); err != nil { 188 return nil, err 189 } 190 } 191 192 s.postingLists = append(s.postingLists, &cachedPostingList{p: p, postingList: searchIndex}) 193 194 return searchIndex, nil 195 }