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
14package navigation
15
16import (
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
31var smc = newMenuCache()
32
33// MenuEntry represents a menu item defined in either Page front matter
34// or in the site config.
35type 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
51func (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.
68type 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.
81type Menu []*MenuEntry
82
83// Menus is a dictionary of menus.
84type Menus map[string]Menu
85
86// PageMenus is a dictionary of menus defined in the Pages.
87type PageMenus map[string]*MenuEntry
88
89// HasChildren returns whether this menu item has any children.
90func (m *MenuEntry) HasChildren() bool {
91	return m.Children != nil
92}
93
94// KeyName returns the key used to identify this menu entry.
95func (m *MenuEntry) KeyName() string {
96	if m.Identifier != "" {
97		return m.Identifier
98	}
99	return m.Name
100}
101
102func (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.
113func (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).
119func (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
127func (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
134func (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
173func (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
185type menuSorter struct {
186	menu Menu
187	by   menuEntryBy
188}
189
190// Closure used in the Sort.Less method.
191type menuEntryBy func(m1, m2 *MenuEntry) bool
192
193func (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
201var 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
221func (ms *menuSorter) Len() int      { return len(ms.menu) }
222func (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.
225func (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.
228func (m Menu) Sort() Menu {
229	menuEntryBy(defaultMenuEntrySort).Sort(m)
230	return m
231}
232
233// Limit limits the returned menu to n entries.
234func (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.
242func (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.
250func (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.
262func (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
274func (m Menu) Clone() Menu {
275	return append(Menu(nil), m...)
276}
277
278func (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}
289