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  }