github.com/gohugoio/hugo@v0.88.1/navigation/menu.go (about) 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 navigation 15 16 import ( 17 "fmt" 18 "html/template" 19 "sort" 20 "strings" 21 22 "github.com/pkg/errors" 23 24 "github.com/gohugoio/hugo/common/maps" 25 "github.com/gohugoio/hugo/common/types" 26 "github.com/gohugoio/hugo/compare" 27 28 "github.com/spf13/cast" 29 ) 30 31 var smc = newMenuCache() 32 33 // MenuEntry represents a menu item defined in either Page front matter 34 // or in the site config. 35 type MenuEntry struct { 36 ConfiguredURL string // The URL value from front matter / config. 37 Page Page 38 PageRef string // The path to the page, only relevant for site config. 39 Name string 40 Menu string 41 Identifier string 42 title string 43 Pre template.HTML 44 Post template.HTML 45 Weight int 46 Parent string 47 Children Menu 48 Params maps.Params 49 } 50 51 func (m *MenuEntry) URL() string { 52 53 // Check page first. 54 // In Hugo 0.86.0 we added `pageRef`, 55 // a way to connect menu items in site config to pages. 56 // This means that you now can have both a Page 57 // and a configured URL. 58 // Having the configured URL as a fallback if the Page isn't found 59 // is obviously more useful, especially in multilingual sites. 60 if !types.IsNil(m.Page) { 61 return m.Page.RelPermalink() 62 } 63 64 return m.ConfiguredURL 65 } 66 67 // A narrow version of page.Page. 68 type Page interface { 69 LinkTitle() string 70 RelPermalink() string 71 Path() string 72 Section() string 73 Weight() int 74 IsPage() bool 75 IsSection() bool 76 IsAncestor(other interface{}) (bool, error) 77 Params() maps.Params 78 } 79 80 // Menu is a collection of menu entries. 81 type Menu []*MenuEntry 82 83 // Menus is a dictionary of menus. 84 type Menus map[string]Menu 85 86 // PageMenus is a dictionary of menus defined in the Pages. 87 type PageMenus map[string]*MenuEntry 88 89 // HasChildren returns whether this menu item has any children. 90 func (m *MenuEntry) HasChildren() bool { 91 return m.Children != nil 92 } 93 94 // KeyName returns the key used to identify this menu entry. 95 func (m *MenuEntry) KeyName() string { 96 if m.Identifier != "" { 97 return m.Identifier 98 } 99 return m.Name 100 } 101 102 func (m *MenuEntry) hopefullyUniqueID() string { 103 if m.Identifier != "" { 104 return m.Identifier 105 } else if m.URL() != "" { 106 return m.URL() 107 } else { 108 return m.Name 109 } 110 } 111 112 // IsEqual returns whether the two menu entries represents the same menu entry. 113 func (m *MenuEntry) IsEqual(inme *MenuEntry) bool { 114 return m.hopefullyUniqueID() == inme.hopefullyUniqueID() && m.Parent == inme.Parent 115 } 116 117 // IsSameResource returns whether the two menu entries points to the same 118 // resource (URL). 119 func (m *MenuEntry) IsSameResource(inme *MenuEntry) bool { 120 if m.isSamePage(inme.Page) { 121 return m.Page == inme.Page 122 } 123 murl, inmeurl := m.URL(), inme.URL() 124 return murl != "" && inmeurl != "" && murl == inmeurl 125 } 126 127 func (m *MenuEntry) isSamePage(p Page) bool { 128 if !types.IsNil(m.Page) && !types.IsNil(p) { 129 return m.Page == p 130 } 131 return false 132 } 133 134 func (m *MenuEntry) MarshallMap(ime map[string]interface{}) error { 135 var err error 136 for k, v := range ime { 137 loki := strings.ToLower(k) 138 switch loki { 139 case "url": 140 m.ConfiguredURL = cast.ToString(v) 141 case "pageref": 142 m.PageRef = cast.ToString(v) 143 case "weight": 144 m.Weight = cast.ToInt(v) 145 case "name": 146 m.Name = cast.ToString(v) 147 case "title": 148 m.title = cast.ToString(v) 149 case "pre": 150 m.Pre = template.HTML(cast.ToString(v)) 151 case "post": 152 m.Post = template.HTML(cast.ToString(v)) 153 case "identifier": 154 m.Identifier = cast.ToString(v) 155 case "parent": 156 m.Parent = cast.ToString(v) 157 case "params": 158 var ok bool 159 m.Params, ok = maps.ToParamsAndPrepare(v) 160 if !ok { 161 err = fmt.Errorf("cannot convert %T to Params", v) 162 } 163 } 164 } 165 166 if err != nil { 167 return errors.Wrapf(err, "failed to marshal menu entry %q", m.KeyName()) 168 } 169 170 return nil 171 } 172 173 func (m Menu) Add(me *MenuEntry) Menu { 174 m = append(m, me) 175 // TODO(bep) 176 m.Sort() 177 return m 178 } 179 180 /* 181 * Implementation of a custom sorter for Menu 182 */ 183 184 // A type to implement the sort interface for Menu 185 type menuSorter struct { 186 menu Menu 187 by menuEntryBy 188 } 189 190 // Closure used in the Sort.Less method. 191 type menuEntryBy func(m1, m2 *MenuEntry) bool 192 193 func (by menuEntryBy) Sort(menu Menu) { 194 ms := &menuSorter{ 195 menu: menu, 196 by: by, // The Sort method's receiver is the function (closure) that defines the sort order. 197 } 198 sort.Stable(ms) 199 } 200 201 var defaultMenuEntrySort = func(m1, m2 *MenuEntry) bool { 202 if m1.Weight == m2.Weight { 203 c := compare.Strings(m1.Name, m2.Name) 204 if c == 0 { 205 return m1.Identifier < m2.Identifier 206 } 207 return c < 0 208 } 209 210 if m2.Weight == 0 { 211 return true 212 } 213 214 if m1.Weight == 0 { 215 return false 216 } 217 218 return m1.Weight < m2.Weight 219 } 220 221 func (ms *menuSorter) Len() int { return len(ms.menu) } 222 func (ms *menuSorter) Swap(i, j int) { ms.menu[i], ms.menu[j] = ms.menu[j], ms.menu[i] } 223 224 // Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. 225 func (ms *menuSorter) Less(i, j int) bool { return ms.by(ms.menu[i], ms.menu[j]) } 226 227 // Sort sorts the menu by weight, name and then by identifier. 228 func (m Menu) Sort() Menu { 229 menuEntryBy(defaultMenuEntrySort).Sort(m) 230 return m 231 } 232 233 // Limit limits the returned menu to n entries. 234 func (m Menu) Limit(n int) Menu { 235 if len(m) > n { 236 return m[0:n] 237 } 238 return m 239 } 240 241 // ByWeight sorts the menu by the weight defined in the menu configuration. 242 func (m Menu) ByWeight() Menu { 243 const key = "menuSort.ByWeight" 244 menus, _ := smc.get(key, menuEntryBy(defaultMenuEntrySort).Sort, m) 245 246 return menus 247 } 248 249 // ByName sorts the menu by the name defined in the menu configuration. 250 func (m Menu) ByName() Menu { 251 const key = "menuSort.ByName" 252 title := func(m1, m2 *MenuEntry) bool { 253 return compare.LessStrings(m1.Name, m2.Name) 254 } 255 256 menus, _ := smc.get(key, menuEntryBy(title).Sort, m) 257 258 return menus 259 } 260 261 // Reverse reverses the order of the menu entries. 262 func (m Menu) Reverse() Menu { 263 const key = "menuSort.Reverse" 264 reverseFunc := func(menu Menu) { 265 for i, j := 0, len(menu)-1; i < j; i, j = i+1, j-1 { 266 menu[i], menu[j] = menu[j], menu[i] 267 } 268 } 269 menus, _ := smc.get(key, reverseFunc, m) 270 271 return menus 272 } 273 274 func (m Menu) Clone() Menu { 275 return append(Menu(nil), m...) 276 } 277 278 func (m *MenuEntry) Title() string { 279 if m.title != "" { 280 return m.title 281 } 282 283 if m.Page != nil { 284 return m.Page.LinkTitle() 285 } 286 287 return "" 288 }