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