github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/page/permalinks.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 page 15 16 import ( 17 "fmt" 18 "os" 19 "path" 20 "path/filepath" 21 "regexp" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/pkg/errors" 27 28 "github.com/gohugoio/hugo/helpers" 29 ) 30 31 // PermalinkExpander holds permalin mappings per section. 32 type PermalinkExpander struct { 33 // knownPermalinkAttributes maps :tags in a permalink specification to a 34 // function which, given a page and the tag, returns the resulting string 35 // to be used to replace that tag. 36 knownPermalinkAttributes map[string]pageToPermaAttribute 37 38 expanders map[string]func(Page) (string, error) 39 40 ps *helpers.PathSpec 41 } 42 43 // Time for checking date formats. Every field is different than the 44 // Go reference time for date formatting. This ensures that formatting this date 45 // with a Go time format always has a different output than the format itself. 46 var referenceTime = time.Date(2019, time.November, 9, 23, 1, 42, 1, time.UTC) 47 48 // Return the callback for the given permalink attribute and a boolean indicating if the attribute is valid or not. 49 func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) { 50 if callback, ok := p.knownPermalinkAttributes[attr]; ok { 51 return callback, true 52 } 53 54 if strings.HasPrefix(attr, "sections[") { 55 fn := p.toSliceFunc(strings.TrimPrefix(attr, "sections")) 56 return func(p Page, s string) (string, error) { 57 return path.Join(fn(p.CurrentSection().SectionsEntries())...), nil 58 }, true 59 } 60 61 // Make sure this comes after all the other checks. 62 if referenceTime.Format(attr) != attr { 63 return p.pageToPermalinkDate, true 64 } 65 66 return nil, false 67 } 68 69 // NewPermalinkExpander creates a new PermalinkExpander configured by the given 70 // PathSpec. 71 func NewPermalinkExpander(ps *helpers.PathSpec) (PermalinkExpander, error) { 72 p := PermalinkExpander{ps: ps} 73 74 p.knownPermalinkAttributes = map[string]pageToPermaAttribute{ 75 "year": p.pageToPermalinkDate, 76 "month": p.pageToPermalinkDate, 77 "monthname": p.pageToPermalinkDate, 78 "day": p.pageToPermalinkDate, 79 "weekday": p.pageToPermalinkDate, 80 "weekdayname": p.pageToPermalinkDate, 81 "yearday": p.pageToPermalinkDate, 82 "section": p.pageToPermalinkSection, 83 "sections": p.pageToPermalinkSections, 84 "title": p.pageToPermalinkTitle, 85 "slug": p.pageToPermalinkSlugElseTitle, 86 "filename": p.pageToPermalinkFilename, 87 } 88 89 patterns := ps.Cfg.GetStringMapString("permalinks") 90 if patterns == nil { 91 return p, nil 92 } 93 94 e, err := p.parse(patterns) 95 if err != nil { 96 return p, err 97 } 98 99 p.expanders = e 100 101 return p, nil 102 } 103 104 // Expand expands the path in p according to the rules defined for the given key. 105 // If no rules are found for the given key, an empty string is returned. 106 func (l PermalinkExpander) Expand(key string, p Page) (string, error) { 107 expand, found := l.expanders[key] 108 109 if !found { 110 return "", nil 111 } 112 113 return expand(p) 114 } 115 116 func (l PermalinkExpander) parse(patterns map[string]string) (map[string]func(Page) (string, error), error) { 117 expanders := make(map[string]func(Page) (string, error)) 118 119 // Allow " " and / to represent the root section. 120 const sectionCutSet = " /" + string(os.PathSeparator) 121 122 for k, pattern := range patterns { 123 k = strings.Trim(k, sectionCutSet) 124 125 if !l.validate(pattern) { 126 return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkIllFormed} 127 } 128 129 pattern := pattern 130 matches := attributeRegexp.FindAllStringSubmatch(pattern, -1) 131 132 callbacks := make([]pageToPermaAttribute, len(matches)) 133 replacements := make([]string, len(matches)) 134 for i, m := range matches { 135 replacement := m[0] 136 attr := replacement[1:] 137 replacements[i] = replacement 138 callback, ok := l.callback(attr) 139 140 if !ok { 141 return nil, &permalinkExpandError{pattern: pattern, err: errPermalinkAttributeUnknown} 142 } 143 144 callbacks[i] = callback 145 } 146 147 expanders[k] = func(p Page) (string, error) { 148 if matches == nil { 149 return pattern, nil 150 } 151 152 newField := pattern 153 154 for i, replacement := range replacements { 155 attr := replacement[1:] 156 callback := callbacks[i] 157 newAttr, err := callback(p, attr) 158 if err != nil { 159 return "", &permalinkExpandError{pattern: pattern, err: err} 160 } 161 162 newField = strings.Replace(newField, replacement, newAttr, 1) 163 164 } 165 166 return newField, nil 167 } 168 169 } 170 171 return expanders, nil 172 } 173 174 // pageToPermaAttribute is the type of a function which, given a page and a tag 175 // can return a string to go in that position in the page (or an error) 176 type pageToPermaAttribute func(Page, string) (string, error) 177 178 var attributeRegexp = regexp.MustCompile(`:\w+(\[.+\])?`) 179 180 // validate determines if a PathPattern is well-formed 181 func (l PermalinkExpander) validate(pp string) bool { 182 fragments := strings.Split(pp[1:], "/") 183 bail := false 184 for i := range fragments { 185 if bail { 186 return false 187 } 188 if len(fragments[i]) == 0 { 189 bail = true 190 continue 191 } 192 193 matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1) 194 if matches == nil { 195 continue 196 } 197 198 for _, match := range matches { 199 k := match[0][1:] 200 if _, ok := l.callback(k); !ok { 201 return false 202 } 203 } 204 } 205 return true 206 } 207 208 type permalinkExpandError struct { 209 pattern string 210 err error 211 } 212 213 func (pee *permalinkExpandError) Error() string { 214 return fmt.Sprintf("error expanding %q: %s", pee.pattern, pee.err) 215 } 216 217 var ( 218 errPermalinkIllFormed = errors.New("permalink ill-formed") 219 errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised") 220 ) 221 222 func (l PermalinkExpander) pageToPermalinkDate(p Page, dateField string) (string, error) { 223 // a Page contains a Node which provides a field Date, time.Time 224 switch dateField { 225 case "year": 226 return strconv.Itoa(p.Date().Year()), nil 227 case "month": 228 return fmt.Sprintf("%02d", int(p.Date().Month())), nil 229 case "monthname": 230 return p.Date().Month().String(), nil 231 case "day": 232 return fmt.Sprintf("%02d", p.Date().Day()), nil 233 case "weekday": 234 return strconv.Itoa(int(p.Date().Weekday())), nil 235 case "weekdayname": 236 return p.Date().Weekday().String(), nil 237 case "yearday": 238 return strconv.Itoa(p.Date().YearDay()), nil 239 } 240 241 return p.Date().Format(dateField), nil 242 } 243 244 // pageToPermalinkTitle returns the URL-safe form of the title 245 func (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error) { 246 return l.ps.URLize(p.Title()), nil 247 } 248 249 // pageToPermalinkFilename returns the URL-safe form of the filename 250 func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, error) { 251 name := p.File().TranslationBaseName() 252 if name == "index" { 253 // Page bundles; the directory name will hopefully have a better name. 254 dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator) 255 _, name = filepath.Split(dir) 256 } 257 258 return l.ps.URLize(name), nil 259 } 260 261 // if the page has a slug, return the slug, else return the title 262 func (l PermalinkExpander) pageToPermalinkSlugElseTitle(p Page, a string) (string, error) { 263 if p.Slug() != "" { 264 return l.ps.URLize(p.Slug()), nil 265 } 266 return l.pageToPermalinkTitle(p, a) 267 } 268 269 func (l PermalinkExpander) pageToPermalinkSection(p Page, _ string) (string, error) { 270 return p.Section(), nil 271 } 272 273 func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, error) { 274 return p.CurrentSection().SectionsPath(), nil 275 } 276 277 var ( 278 nilSliceFunc = func(s []string) []string { 279 return nil 280 } 281 allSliceFunc = func(s []string) []string { 282 return s 283 } 284 ) 285 286 // toSliceFunc returns a slice func that slices s according to the cut spec. 287 // The cut spec must be on form [low:high] (one or both can be omitted), 288 // also allowing single slice indices (e.g. [2]) and the special [last] keyword 289 // giving the last element of the slice. 290 // The returned function will be lenient and not panic in out of bounds situation. 291 // 292 // The current use case for this is to use parts of the sections path in permalinks. 293 func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string { 294 cut = strings.ToLower(strings.TrimSpace(cut)) 295 if cut == "" { 296 return allSliceFunc 297 } 298 299 if len(cut) < 3 || (cut[0] != '[' || cut[len(cut)-1] != ']') { 300 return nilSliceFunc 301 } 302 303 toNFunc := func(s string, low bool) func(ss []string) int { 304 if s == "" { 305 if low { 306 return func(ss []string) int { 307 return 0 308 } 309 } else { 310 return func(ss []string) int { 311 return len(ss) 312 } 313 } 314 } 315 316 if s == "last" { 317 return func(ss []string) int { 318 return len(ss) - 1 319 } 320 } 321 322 n, _ := strconv.Atoi(s) 323 if n < 0 { 324 n = 0 325 } 326 return func(ss []string) int { 327 // Prevent out of bound situations. It would not make 328 // much sense to panic here. 329 if n > len(ss) { 330 return len(ss) 331 } 332 return n 333 } 334 } 335 336 opsStr := cut[1 : len(cut)-1] 337 opts := strings.Split(opsStr, ":") 338 339 if !strings.Contains(opsStr, ":") { 340 toN := toNFunc(opts[0], true) 341 return func(s []string) []string { 342 if len(s) == 0 { 343 return nil 344 } 345 v := s[toN(s)] 346 if v == "" { 347 return nil 348 } 349 return []string{v} 350 } 351 } 352 353 toN1, toN2 := toNFunc(opts[0], true), toNFunc(opts[1], false) 354 355 return func(s []string) []string { 356 if len(s) == 0 { 357 return nil 358 } 359 return s[toN1(s):toN2(s)] 360 } 361 362 }