github.com/pietrocarrara/hugo@v0.47.1/hugolib/permalinks.go (about) 1 // Copyright 2015 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 hugolib 15 16 import ( 17 "errors" 18 "fmt" 19 "path" 20 "path/filepath" 21 "regexp" 22 "strconv" 23 "strings" 24 25 "github.com/gohugoio/hugo/helpers" 26 ) 27 28 // pathPattern represents a string which builds up a URL from attributes 29 type pathPattern string 30 31 // pageToPermaAttribute is the type of a function which, given a page and a tag 32 // can return a string to go in that position in the page (or an error) 33 type pageToPermaAttribute func(*Page, string) (string, error) 34 35 // PermalinkOverrides maps a section name to a PathPattern 36 type PermalinkOverrides map[string]pathPattern 37 38 // knownPermalinkAttributes maps :tags in a permalink specification to a 39 // function which, given a page and the tag, returns the resulting string 40 // to be used to replace that tag. 41 var knownPermalinkAttributes map[string]pageToPermaAttribute 42 43 var attributeRegexp *regexp.Regexp 44 45 // validate determines if a PathPattern is well-formed 46 func (pp pathPattern) validate() bool { 47 fragments := strings.Split(string(pp[1:]), "/") 48 var bail = false 49 for i := range fragments { 50 if bail { 51 return false 52 } 53 if len(fragments[i]) == 0 { 54 bail = true 55 continue 56 } 57 58 matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1) 59 if matches == nil { 60 continue 61 } 62 63 for _, match := range matches { 64 k := strings.ToLower(match[0][1:]) 65 if _, ok := knownPermalinkAttributes[k]; !ok { 66 return false 67 } 68 } 69 } 70 return true 71 } 72 73 type permalinkExpandError struct { 74 pattern pathPattern 75 section string 76 err error 77 } 78 79 func (pee *permalinkExpandError) Error() string { 80 return fmt.Sprintf("error expanding %q section %q: %s", string(pee.pattern), pee.section, pee.err) 81 } 82 83 var ( 84 errPermalinkIllFormed = errors.New("permalink ill-formed") 85 errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised") 86 ) 87 88 // Expand on a PathPattern takes a Page and returns the fully expanded Permalink 89 // or an error explaining the failure. 90 func (pp pathPattern) Expand(p *Page) (string, error) { 91 if !pp.validate() { 92 return "", &permalinkExpandError{pattern: pp, section: "<all>", err: errPermalinkIllFormed} 93 } 94 sections := strings.Split(string(pp), "/") 95 for i, field := range sections { 96 if len(field) == 0 { 97 continue 98 } 99 100 matches := attributeRegexp.FindAllStringSubmatch(field, -1) 101 102 if matches == nil { 103 continue 104 } 105 106 newField := field 107 108 for _, match := range matches { 109 attr := match[0][1:] 110 callback, ok := knownPermalinkAttributes[attr] 111 112 if !ok { 113 return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: errPermalinkAttributeUnknown} 114 } 115 116 newAttr, err := callback(p, attr) 117 118 if err != nil { 119 return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: err} 120 } 121 122 newField = strings.Replace(newField, match[0], newAttr, 1) 123 } 124 125 sections[i] = newField 126 } 127 return strings.Join(sections, "/"), nil 128 } 129 130 func pageToPermalinkDate(p *Page, dateField string) (string, error) { 131 // a Page contains a Node which provides a field Date, time.Time 132 switch dateField { 133 case "year": 134 return strconv.Itoa(p.Date.Year()), nil 135 case "month": 136 return fmt.Sprintf("%02d", int(p.Date.Month())), nil 137 case "monthname": 138 return p.Date.Month().String(), nil 139 case "day": 140 return fmt.Sprintf("%02d", p.Date.Day()), nil 141 case "weekday": 142 return strconv.Itoa(int(p.Date.Weekday())), nil 143 case "weekdayname": 144 return p.Date.Weekday().String(), nil 145 case "yearday": 146 return strconv.Itoa(p.Date.YearDay()), nil 147 } 148 //TODO: support classic strftime escapes too 149 // (and pass those through despite not being in the map) 150 panic("coding error: should not be here") 151 } 152 153 // pageToPermalinkTitle returns the URL-safe form of the title 154 func pageToPermalinkTitle(p *Page, _ string) (string, error) { 155 // Page contains Node which has Title 156 // (also contains URLPath which has Slug, sometimes) 157 return p.s.PathSpec.URLize(p.title), nil 158 } 159 160 // pageToPermalinkFilename returns the URL-safe form of the filename 161 func pageToPermalinkFilename(p *Page, _ string) (string, error) { 162 name := p.File.TranslationBaseName() 163 if name == "index" { 164 // Page bundles; the directory name will hopefully have a better name. 165 dir := strings.TrimSuffix(p.File.Dir(), helpers.FilePathSeparator) 166 _, name = filepath.Split(dir) 167 } 168 169 return p.s.PathSpec.URLize(name), nil 170 } 171 172 // if the page has a slug, return the slug, else return the title 173 func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) { 174 if p.Slug != "" { 175 // Don't start or end with a - 176 // TODO(bep) this doesn't look good... Set the Slug once. 177 if strings.HasPrefix(p.Slug, "-") { 178 p.Slug = p.Slug[1:len(p.Slug)] 179 } 180 181 if strings.HasSuffix(p.Slug, "-") { 182 p.Slug = p.Slug[0 : len(p.Slug)-1] 183 } 184 return p.s.PathSpec.URLize(p.Slug), nil 185 } 186 return pageToPermalinkTitle(p, a) 187 } 188 189 func pageToPermalinkSection(p *Page, _ string) (string, error) { 190 // Page contains Node contains URLPath which has Section 191 return p.Section(), nil 192 } 193 194 func pageToPermalinkSections(p *Page, _ string) (string, error) { 195 // TODO(bep) we have some superflous URLize in this file, but let's 196 // deal with that later. 197 return path.Join(p.CurrentSection().sections...), nil 198 } 199 200 func init() { 201 knownPermalinkAttributes = map[string]pageToPermaAttribute{ 202 "year": pageToPermalinkDate, 203 "month": pageToPermalinkDate, 204 "monthname": pageToPermalinkDate, 205 "day": pageToPermalinkDate, 206 "weekday": pageToPermalinkDate, 207 "weekdayname": pageToPermalinkDate, 208 "yearday": pageToPermalinkDate, 209 "section": pageToPermalinkSection, 210 "sections": pageToPermalinkSections, 211 "title": pageToPermalinkTitle, 212 "slug": pageToPermalinkSlugElseTitle, 213 "filename": pageToPermalinkFilename, 214 } 215 216 attributeRegexp = regexp.MustCompile(`:\w+`) 217 }