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